Module: DaVinciDTRTestKit::CQLTest

Constant Summary collapse

STATIC_QUESTIONNAIRE_PACKAGE_BUNDLE_PROFILE =
'http://hl7.org/fhir/us/davinci-dtr/StructureDefinition/DTR-QPackageBundle|2.0.1'.freeze
STATIC_QUESTIONNAIRE_PACKAGE_PARAMETER_PROFILE =
'http://hl7.org/fhir/us/davinci-dtr/StructureDefinition/dtr-qpackage-output-parameters|2.0.1'.freeze
ADAPTIVE_QUESTIONNAIRE_PACKAGE_BUNDLE_PROFILE =
'http://hl7.org/fhir/us/davinci-dtr/StructureDefinition/DTR-QPackageBundle|2.0.1'.freeze
ADAPTIVE_QUESTIONNAIRE_PACKAGE_PARAMETER_PROFILE =
'http://hl7.org/fhir/us/davinci-dtr/StructureDefinition/dtr-qpackage-output-parameters|2.0.1'.freeze

Instance Method Summary collapse

Instance Method Details

#add_formatting_messages(misformatted_expressions, q_index) ⇒ Object



151
152
153
154
155
156
157
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 151

def add_formatting_messages(misformatted_expressions, q_index)
  misformatted_expressions.compact.each do |idx|
    messages << { type: 'info',
                  message: format_markdown("[expression #{idx + 1}] in [questionnaire #{q_index + 1}]
                  does not begin with a reference to an included library name.") }
  end
end

#add_item_messages(found_item_expressions, q_index) ⇒ Object



169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 169

def add_item_messages(found_item_expressions, q_index)
  unless found_item_expressions['found_candidate_expression']
    messages << { type: 'info',
                  message: format_markdown("[questionnaire #{q_index + 1}] included no candidate expression.") }
  end
  unless found_item_expressions['found_init_expression']
    messages << { type: 'info',
                  message: format_markdown("[questionnaire #{q_index + 1}] included no initial expression.") }
  end
  return if found_item_expressions['found_context_expression']

  messages << { type: 'info',
                message: format_markdown("[questionnaire #{q_index + 1}] included no context expression.") }
end

#add_launch_context_messages(found_launch_context, found_variable, found_pop_context, found_cqf_lib, q_index) ⇒ Object



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 129

def add_launch_context_messages(found_launch_context, found_variable, found_pop_context, found_cqf_lib, q_index)
  unless found_launch_context
    messages << { type: 'info',
                  message: format_markdown("[questionnaire #{q_index + 1}] included no launch context.") }
  end
  unless found_variable
    messages << { type: 'info',
                  message: format_markdown("[questionnaire #{q_index + 1}]
                   included no variable to query for additional data.") }
  end
  unless found_pop_context
    messages << { type: 'info',
                  message: format_markdown("[questionnaire #{q_index + 1}]
                   included no item population context.") }
  end
  return if found_cqf_lib

  messages << { type: 'info',
                message: format_markdown("[questionnaire #{q_index + 1}]
                  included no cqf library.") }
end

#check_expression_format(item_ext, index) ⇒ Object



294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 294

def check_expression_format(item_ext, index)
  return unless library_names.none?

  expression_passes = false
  library_names.each do |name|
    if item_ext.valueExpression.expression.start_with? "\"#{name}\""
      expression_passes = true
      break
    end
  end
  index unless expression_passes
end

#check_for_cql(extension, extension_name, index, q_index, url, link_id = '') ⇒ Object



307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 307

def check_for_cql(extension, extension_name, index, q_index, url, link_id = '')
  return if extension.valueExpression.nil?
  return if extension.valueExpression.language == 'text/cql'

  cql_presence[extension_name] = false unless extension_name.blank?
  messages << if link_id.blank?
                { type: 'info',
                  message: format_markdown("[extension #{index + 1}] in [questionnaire #{q_index + 1}]
                      contains expression that does not have content type of cql
                      (URL: #{url}).") }
              else
                { type: 'info',
                  message: format_markdown("[item #{index + 1}] in [questionnaire #{q_index + 1}]
                      contains expression that does not have content type of cql
                      (linkId: #{link_id}, URL: #{url}).") }
              end
end

#check_item_extension(item_ext, index, q_index, found_item_expressions, link_id) ⇒ Object



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 260

def check_item_extension(item_ext, index, q_index, found_item_expressions, link_id)
  if item_ext.url == 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-candidateExpression'
    found_item_expressions['found_candidate_expression'] = true
    extension_presence['found_min_candidate_expression'] = true
    check_for_cql(item_ext, 'candidate_expression', index, q_index, item_ext.url, link_id)
    return check_expression_format(item_ext, index)
  end
  if item_ext.url == 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression'
    found_item_expressions['found_init_expression'] = true
    extension_presence['found_min_init_expression'] = true
    check_for_cql(item_ext, 'init_expression', index, q_index, item_ext.url, link_id)
    return check_expression_format(item_ext, index)
  end
  if item_ext.url == 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-contextExpression'
    found_item_expressions['found_context_expression'] = true
    extension_presence['found_min_context_expression'] = true
    check_for_cql(item_ext, 'context_expression', index, q_index, item_ext.url, link_id)
    return check_expression_format(item_ext, index)
  end
  check_for_cql(item_ext, '', index, q_index, item_ext.url, link_id)
end

#check_libraries(questionnaire_bundles) ⇒ Object



240
241
242
243
244
245
246
247
248
249
250
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 240

def check_libraries(questionnaire_bundles)
  libraries = extract_libraries_from_bundles(questionnaire_bundles)

  assert libraries.any?, 'No Libraries found.'

  libraries.each do |lib|
    library_urls.add(lib.url) unless lib.url.nil?
    evaluate_library(lib)
    library_names.add(lib.name)
  end
end

#check_library_referencesObject



252
253
254
255
256
257
258
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 252

def check_library_references
  missing_references = cqf_reference_libraries.select do |url|
    library_urls.exclude? url
  end
  assert missing_references.empty?,
         "Some libraries referenced by cqf-libraries were not found: #{missing_references.join(', ')}"
end

#check_nested_items(item, index, q_index, found_item_expressions, link_id) ⇒ Object



282
283
284
285
286
287
288
289
290
291
292
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 282

def check_nested_items(item, index, q_index, found_item_expressions, link_id)
  misformatted_nested_expressions = []
  item.item.each do |nested_item|
    check_nested_items(nested_item, index, q_index, found_item_expressions, nested_item.linkId)
    nested_item.extension.each do |item_ext|
      misformatted_nested_expressions << check_item_extension(item_ext, index, q_index, found_item_expressions,
                                                              link_id)
    end
  end
  misformatted_nested_expressions.compact
end

#check_questionnaire_extensions(questionnaire, q_index) ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 75

def check_questionnaire_extensions(questionnaire, q_index)
  # are extensions present in this questionnaire?
  found_launch_context = found_variable = found_pop_context = found_cqf_lib = false
  cqf_count = total_cqf_libs(questionnaire.extension)
  misformatted_expressions = []
  questionnaire.extension.each_with_index do |extension, index|
    if extension.url == 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-launchContext'
      found_launch_context = true
      extension_presence['found_min_launch_context'] = true
      check_for_cql(extension, 'launch_context', index, q_index, extension.url)
      misformatted_expressions << check_expression_format(extension, index)
    end
    if extension.url == 'http://hl7.org/fhir/StructureDefinition/variable'
      found_variable = true
      extension_presence['found_min_variable'] = true
      check_for_cql(extension, 'variable', index, q_index, extension.url)
      misformatted_expressions << check_expression_format(extension, index)
    end
    if extension.url == 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-itemPopulationContext'
      found_pop_context = true
      extension_presence['found_min_pop_context'] = true
      check_for_cql(extension, 'pop_context', index, q_index, extension.url)
      misformatted_expressions << check_expression_format(extension, index)
    end
    next unless extension.url == 'http://hl7.org/fhir/StructureDefinition/cqf-library'

    cqf_reference_libraries.add(extension.valueCanonical)
    found_cqf_lib = true
    extension_presence['found_min_cqf_lib'] = true

    check_for_cql(extension, '', index, q_index, extension.url)
  end
  add_launch_context_messages(found_launch_context, found_variable, found_pop_context, found_cqf_lib, q_index)
  return if cqf_count < 1

  add_formatting_messages(misformatted_expressions, q_index)
  assert misformatted_expressions.compact.empty?, 'Expression in questionnaire misformatted.'
end

#check_questionnaire_items(questionnaire, q_index) ⇒ Object



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 184

def check_questionnaire_items(questionnaire, q_index)
  # are expressions present in this questionnaire?
  found_item_expressions = { 'found_init_expression' => false,
                             'found_candidate_expression' => false,
                             'found_context_expression' => false }
  cqf_count = total_cqf_libs(questionnaire.extension)
  misformatted_expressions = []

  # check questionnaire items
  questionnaire.item.each_with_index do |item, index|
    misformatted_expressions.concat(check_nested_items(item, index, q_index, found_item_expressions, item.linkId))
    # check extensions on items
    item.extension.each do |item_ext|
      misformatted_expressions << check_item_extension(item_ext,
                                                       index, q_index, found_item_expressions, item.linkId)
    end
  end
  add_item_messages(found_item_expressions, q_index)
  # only care about formatting when there are multiple cqf libs
  return if cqf_count < 1

  misformatted_expressions.compact.to_set.each do |idx|
    messages << { type: 'info',
                  message: format_markdown("[item #{idx + 1}] in [questionnaire #{q_index + 1}]
                  contains expression that does not begin with a reference to an included library name.") }
  end
  assert misformatted_expressions.compact.to_set.empty?, 'Expression in questionnaire misformatted.'
end

#cqf_reference_librariesObject



30
31
32
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 30

def cqf_reference_libraries
  scratch[:cqf_reference_libraries] ||= Set.new
end

#cql_presenceObject



24
25
26
27
28
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 24

def cql_presence
  @cql_presence ||= { 'launch_context' => true, 'variable' => true,
                      'pop_context' => true, 'init_expression' => true,
                      'candidate_expression' => true, 'context_expression' => true }
end

#evaluate_library(library) ⇒ Object



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 213

def evaluate_library(library)
  found_cql = found_elm = false
  library.content.each do |content|
    if content.data.nil?
      messages << { type: 'info',
                    message: format_markdown("[library #{library.url}] content element included no data.") }
    end
    if content.contentType == 'text/cql'
      found_cql = true
    elsif content.contentType == 'application/elm+json'
      found_elm = true
    else
      messages << { type: 'info',
                    message: format_markdown("[library #{library.url}] has non-cql/elm content.") }
      true
    end
    next unless library_names.include? library.name

    found_duplicate_library_name = true
    messages << { type: 'info', message: format_markdown("[library #{library.url}] has a name,
     #{library.name}, that is already included in the bundle.") }
    assert !found_duplicate_library_name, 'Found duplicate library names - all names must be unique.'
  end
  assert found_cql, "[library #{library.url}] does not include CQL."
  assert found_elm, "[library #{library.url}] does not include ELM."
end

#extension_presenceObject



17
18
19
20
21
22
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 17

def extension_presence
  @extension_presence ||= { 'found_min_launch_context' => false, 'found_min_variable' => false,
                            'found_min_pop_context' => false, 'found_min_init_expression' => false,
                            'found_min_candidate_expression' => false, 'found_min_context_expression' => false,
                            'found_min_cqf_lib' => false }
end

#extract_bundles_from_parameter(parameter) ⇒ Object



365
366
367
368
369
370
371
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 365

def extract_bundles_from_parameter(parameter)
  return [] if parameter.blank?

  parameter.parameter&.filter_map do |param|
    param.resource if param.resource&.resourceType == 'Bundle'
  end
end

#extract_contained_questionnaires(questionnaire_responses) ⇒ Object



325
326
327
328
329
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 325

def extract_contained_questionnaires(questionnaire_responses)
  questionnaire_responses&.filter_map do |qr|
    qr.contained&.filter { |resource| resource.is_a?(FHIR::Questionnaire) }
  end&.flatten&.compact
end

#extract_libraries_from_bundles(questionnaire_bundles) ⇒ Object



381
382
383
384
385
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 381

def extract_libraries_from_bundles(questionnaire_bundles)
  questionnaire_bundles.filter_map do |qb|
    qb.entry.filter_map { |entry| entry.resource if entry&.resource.is_a?(FHIR::Library) }
  end&.flatten&.compact
end

#extract_questionnaire_bundles(resource) ⇒ Object



354
355
356
357
358
359
360
361
362
363
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 354

def extract_questionnaire_bundles(resource)
  case resource&.resourceType
  when 'Bundle'
    [resource]
  when 'Parameters'
    extract_bundles_from_parameter(resource)
  else
    []
  end
end

#extract_questionnaire_from_questionnaire_package(questionnaire_pkg_json, questionnaire_url) ⇒ Object



373
374
375
376
377
378
379
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 373

def extract_questionnaire_from_questionnaire_package(questionnaire_pkg_json, questionnaire_url)
  resource = FHIR.from_contents(questionnaire_pkg_json)
  questionnaire_bundles = extract_questionnaire_bundles(resource)
  questionnaires = extract_questionnaires_from_bundles(questionnaire_bundles)

  questionnaires.find { |q| q.url == questionnaire_url }
end

#extract_questionnaires_from_bundles(questionnaire_bundles) ⇒ Object



331
332
333
334
335
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 331

def extract_questionnaires_from_bundles(questionnaire_bundles)
  questionnaire_bundles.filter_map do |qb|
    qb.entry.filter_map { |entry| entry.resource if entry.resource.is_a?(FHIR::Questionnaire) }
  end&.flatten&.compact
end

#found_duplicate_library_nameObject



46
47
48
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 46

def found_duplicate_library_name
  @found_duplicate_library_name ||= false
end

#found_non_cql_elm_libraryObject



50
51
52
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 50

def found_non_cql_elm_library
  @found_non_cql_elm_library ||= false
end

#found_non_cql_expressionObject



54
55
56
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 54

def found_non_cql_expression
  @found_non_cql_expression ||= false
end

#found_questionnaireObject



42
43
44
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 42

def found_questionnaire
  @found_questionnaire ||= false
end

#library_namesObject



38
39
40
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 38

def library_names
  scratch[:library_names] ||= Set.new
end

#library_urlsObject



34
35
36
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 34

def library_urls
  scratch[:library_urls] ||= Set.new
end

#perform_questionnaire_package_validation(resource, form = 'static') ⇒ Object



337
338
339
340
341
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 337

def perform_questionnaire_package_validation(resource, form = 'static')
  scratch[:"#{form}_questionnaire_bundles"] = extract_questionnaire_bundles(resource)
  validate_questionnaire_package(resource, form)
  assert scratch[:"#{form}_questionnaire_bundles"].present?, 'No questionnaire bundle found in the response'
end

#questionnaire_package_profile_urlsObject



8
9
10
11
12
13
14
15
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 8

def questionnaire_package_profile_urls
  {
    static_bundle: STATIC_QUESTIONNAIRE_PACKAGE_BUNDLE_PROFILE,
    static_parameter: STATIC_QUESTIONNAIRE_PACKAGE_PARAMETER_PROFILE,
    adaptive_bundle: ADAPTIVE_QUESTIONNAIRE_PACKAGE_BUNDLE_PROFILE,
    adaptive_parameter: ADAPTIVE_QUESTIONNAIRE_PACKAGE_PARAMETER_PROFILE
  }
end

#reset_cql_testsObject



58
59
60
61
62
63
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 58

def reset_cql_tests
  library_names.clear
  library_urls.clear
  cqf_reference_libraries.clear
  extension_presence.each_key { |k| extension_presence[k] = false }
end

#total_cqf_libs(extensions) ⇒ Object



159
160
161
162
163
164
165
166
167
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 159

def total_cqf_libs(extensions)
  cqf_count = 0
  extensions.each do |extension|
    next unless extension.url == 'http://hl7.org/fhir/StructureDefinition/cqf-library'

    cqf_count += 1
  end
  cqf_count
end

#validate_questionnaire_package(resource, form) ⇒ Object



343
344
345
346
347
348
349
350
351
352
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 343

def validate_questionnaire_package(resource, form)
  case resource&.resourceType
  when 'Bundle'
    assert_valid_resource(resource:, profile_url: questionnaire_package_profile_urls[:"#{form}_bundle"])
  when 'Parameters'
    assert_valid_resource(resource:, profile_url: questionnaire_package_profile_urls[:"#{form}_parameter"])
  else
    assert(false, "Unexpected resourceType: #{resource&.resourceType}. Expected Parameters or Bundle")
  end
end

#verify_questionnaire_extensions(questionnaires) ⇒ Object



65
66
67
68
69
70
71
72
73
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 65

def verify_questionnaire_extensions(questionnaires)
  assert questionnaires&.any? && questionnaires.all? { |q| q.is_a? FHIR::Questionnaire }, 'No questionnaires found.'
  questionnaires.each_with_index { |q, q_index| check_questionnaire_extensions(q, q_index) }
  check_library_references
  assert extension_presence.value?(true), 'No extensions found. Questionnaire must demonstrate prepopulation.'
  assert cql_presence['variable'], 'Variable expression logic not written in CQL.'
  assert cql_presence['launch_context'], 'Launch context expression logic not written in CQL.'
  assert cql_presence['pop_context'], 'Population context expression logic not written in CQL.'
end

#verify_questionnaire_items(questionnaires, final_cql_test: false) ⇒ Object



114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/davinci_dtr_test_kit/cql_test.rb', line 114

def verify_questionnaire_items(questionnaires, final_cql_test: false)
  assert questionnaires&.any? && questionnaires.all? { |q| q.is_a? FHIR::Questionnaire }, 'No questionnaires found.'
  questionnaires.each_with_index { |q, q_index| check_questionnaire_items(q, q_index) }

  begin
    assert !found_non_cql_expression, 'Found non-cql expression.'
    assert extension_presence.value?(true), 'No extensions found. Questionnaire must demonstrate prepopulation.'
    assert cql_presence['init_expression'], 'Initial expression logic not written in CQL.'
    assert cql_presence['candidate_expression'], 'Candidate expression logic not written in CQL.'
    assert cql_presence['context_expression'], 'Context expression logic not written in CQL.'
  ensure
    reset_cql_tests if final_cql_test
  end
end