Class: Inferno::DSL::FHIRResourceValidation::Validator

Inherits:
Object
  • Object
show all
Defined in:
lib/inferno/dsl/fhir_resource_validation.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name = nil, test_suite_id = nil, requirements = nil) ⇒ Validator

Returns a new instance of Validator.



46
47
48
49
50
51
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 46

def initialize(name = nil, test_suite_id = nil, requirements = nil, &)
  @name = name
  @test_suite_id = test_suite_id
  instance_eval(&)
  @requirements = requirements
end

Instance Attribute Details

#nameObject

Returns the value of attribute name.



43
44
45
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 43

def name
  @name
end

#requirementsObject (readonly)

Returns the value of attribute requirements.



42
43
44
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 42

def requirements
  @requirements
end

#session_idObject

Returns the value of attribute session_id.



43
44
45
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 43

def session_id
  @session_id
end

#test_suite_idObject

Returns the value of attribute test_suite_id.



43
44
45
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 43

def test_suite_id
  @test_suite_id
end

Instance Method Details

#additional_validation_messages(resource, profile_url) ⇒ Object



155
156
157
158
159
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 155

def additional_validation_messages(resource, profile_url)
  additional_validations
    .flat_map { |step| step.call(resource, profile_url) }
    .select { |message| message.is_a? Hash }
end

#additional_validationsObject



127
128
129
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 127

def additional_validations
  @additional_validations ||= []
end

#call_validator(resource, profile_url) ⇒ Object



305
306
307
308
309
310
311
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 305

def call_validator(resource, profile_url)
  request_body = wrap_resource_for_hl7_wrapper(resource, profile_url)
  Faraday.new(
    url,
    request: { timeout: 600 }
  ).post('validate', request_body, content_type: 'application/json')
end

#default_validator_urlObject



54
55
56
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 54

def default_validator_url
  ENV.fetch('FHIR_RESOURCE_VALIDATOR_URL')
end

#exclude_message {|message| ... } ⇒ Object

Filter out unwanted validation messages. Any messages for which the block evalutates to a truthy value will be excluded.

Examples:

validator do
  exclude_message { |message| message.type == 'info' }
end

Yield Parameters:



169
170
171
172
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 169

def exclude_message(&block)
  @exclude_message = block if block_given?
  @exclude_message
end

#exclude_unresolved_url_messageObject



212
213
214
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 212

def exclude_unresolved_url_message
  proc { |message| message.message.match?(/\A\S+: [^:]+: URL value '.*' does not resolve/) }
end

#filter_messages(message_hashes) ⇒ Object



217
218
219
220
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 217

def filter_messages(message_hashes)
  message_hashes.reject! { |message| exclude_unresolved_url_message.call(Entities::Message.new(message)) }
  message_hashes.reject! { |message| exclude_message.call(Entities::Message.new(message)) } if exclude_message
end

#igs(*validator_igs) ⇒ Object

Set the IGs that the validator will need to load

Examples:

igs "hl7.fhir.us.core#4.0.0"
igs("hl7.fhir.us.core#3.1.1", "hl7.fhir.us.core#6.0.0")

Parameters:

  • validator_igs (Array<String>)


77
78
79
80
81
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 77

def igs(*validator_igs)
  validation_context(igs: validator_igs) if validator_igs.any?

  validation_context.igs
end

#issue_message(issue, resource) ⇒ Object



254
255
256
257
258
259
260
261
262
263
264
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 254

def issue_message(issue, resource)
  location = if issue.respond_to?(:expression)
               issue.expression&.join(', ')
             else
               issue.location&.join(', ')
             end

  location_prefix = resource.id ? "#{resource.resourceType}/#{resource.id}" : resource.resourceType

  "#{location_prefix}: #{location}: #{issue&.details&.text}"
end

#issue_severity(issue) ⇒ Object



242
243
244
245
246
247
248
249
250
251
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 242

def issue_severity(issue)
  case issue.severity
  when 'warning'
    'warning'
  when 'information'
    'info'
  else
    'error'
  end
end

#message_hash_from_issue(issue, resource) ⇒ Object



234
235
236
237
238
239
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 234

def message_hash_from_issue(issue, resource)
  {
    type: issue_severity(issue),
    message: issue_message(issue, resource)
  }
end

#message_hashes_from_outcome(outcome, resource, profile_url) ⇒ Object



223
224
225
226
227
228
229
230
231
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 223

def message_hashes_from_outcome(outcome, resource, profile_url)
  message_hashes = outcome.issue&.map { |issue| message_hash_from_issue(issue, resource) } || []

  message_hashes.concat(additional_validation_messages(resource, profile_url))

  filter_messages(message_hashes)

  message_hashes
end

#operation_outcome_from_hl7_wrapped_response(response_hash) ⇒ Object



314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 314

def operation_outcome_from_hl7_wrapped_response(response_hash)
  # This is a workaround for some test kits which for legacy reasons
  # call this method directly with a String instead of a Hash.
  # See FI-3178.
  response_hash = JSON.parse(remove_invalid_characters(response_hash)) if response_hash.is_a? String

  if response_hash['sessionId'] && response_hash['sessionId'] != @session_id
    validator_session_repo.save(test_suite_id:, validator_session_id: response_hash['sessionId'],
                                validator_name: name.to_s, suite_options: requirements)
    @session_id = response_hash['sessionId']
  end

  # assume for now that one resource -> one request
  issues = (response_hash.dig('outcomes', 0, 'issues') || []).map do |i|
    { severity: i['level'].downcase, expression: i['location'], details: { text: i['message'] } }
  end
  # this is circuitous, ideally we would map this response directly to message_hashes
  FHIR::OperationOutcome.new(issue: issues)
end

#operation_outcome_from_validator_response(response, runnable) ⇒ Object



340
341
342
343
344
345
346
347
348
349
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 340

def operation_outcome_from_validator_response(response, runnable)
  sanitized_body = remove_invalid_characters(response.body)

  operation_outcome_from_hl7_wrapped_response(JSON.parse(sanitized_body))
rescue JSON::ParserError
  runnable.add_message('error', "Validator Response: HTTP #{response.status}\n#{sanitized_body}")
  raise Inferno::Exceptions::ErrorInValidatorException,
        'Validator response was an unexpected format. ' \
        'Review Messages tab or validator service logs for more information.'
end

#perform_additional_validation {|resource, profile_url| ... } ⇒ Object

Perform validation steps in addition to FHIR validation.

Examples:

perform_additional_validation do |resource, profile_url|
  if something_is_wrong
    { type: 'error', message: 'something is wrong' }
  else
    { type: 'info', message: 'everything is ok' }
  end
end

Yield Parameters:

  • resource (FHIR::Model)

    the resource being validated

  • profile_url (String)

    the profile the resource is being validated against

Yield Returns:

  • (Array<Hash<Symbol, String>>, Hash<Symbol, String>)

    The block should return a Hash or an Array of Hashes if any validation messages should be added. The Hash must contain two keys: ‘:type` and `:message`. `:type` can have a value of `’info’‘, `’warning’‘, or `’error’‘. A type of `’error’‘ means the resource is invalid. `:message` contains the message string itself.



150
151
152
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 150

def perform_additional_validation(&block)
  additional_validations << block
end

#remove_invalid_characters(string) ⇒ Object



335
336
337
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 335

def remove_invalid_characters(string)
  string.gsub(/[^[:print:]\r\n]+/, '')
end

#resource_is_valid?(resource, profile_url, runnable, add_messages_to_runnable: true) ⇒ Boolean

Returns:

  • (Boolean)

See Also:

  • Inferno::DSL::FHIRResourceValidation#resource_is_valid?


175
176
177
178
179
180
181
182
183
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
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 175

def resource_is_valid?(resource, profile_url, runnable, add_messages_to_runnable: true) # rubocop:disable Metrics/CyclomaticComplexity
  profile_url ||= FHIR::Definitions.resource_definition(resource.resourceType).url

  begin
    response = call_validator(resource, profile_url)
  rescue StandardError => e
    runnable.add_message('error', e.message)
    Application[:logger].error(e.message)

    raise Inferno::Exceptions::ErrorInValidatorException, validator_error_message(e)
  end

  outcome = operation_outcome_from_validator_response(response, runnable)

  message_hashes = message_hashes_from_outcome(outcome, resource, profile_url)

  if add_messages_to_runnable
    message_hashes
      .each { |message_hash| runnable.add_message(message_hash[:type], message_hash[:message]) }
  end

  unless response.status == 200
    raise Inferno::Exceptions::ErrorInValidatorException,
          'Error occurred in the validator. Review Messages tab or validator service logs for more information.'
  end

  message_hashes
    .none? { |message_hash| message_hash[:type] == 'error' }
rescue Inferno::Exceptions::ErrorInValidatorException
  raise
rescue StandardError => e
  runnable.add_message('error', e.message)
  raise Inferno::Exceptions::ErrorInValidatorException,
        'Error occurred in the validator. Review Messages tab or validator service logs for more information.'
end

#url(validator_url = nil) ⇒ Object

Set the url of the validator service

Parameters:

  • validator_url (String) (defaults to: nil)


65
66
67
68
69
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 65

def url(validator_url = nil)
  @url = validator_url if validator_url
  @url ||= default_validator_url
  @url
end

#validate(resource, profile_url) ⇒ String

Post a resource to the validation service for validating.

Parameters:

Returns:

  • (String)

    the body of the validation response



300
301
302
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 300

def validate(resource, profile_url)
  call_validator(resource, profile_url).body
end

#validation_context(definition = nil) ⇒ Object Also known as: cli_context

Set the validationContext used as part of each validation request. Fields may be passed as either a Hash or block. Note that all fields included here will be sent directly in requests, there is no check that the fields are correct.

Examples:

# Passing fields in a block
fhir_resource_validator do
  url 'http://example.com/validator'
  validation_context do
    noExtensibleBindingMessages true
    allowExampleUrls true
    txServer nil
  end
end
# Passing fields in a Hash
fhir_resource_validator do
  url 'http://example.org/validator'
  validation_context({
    noExtensibleBindingMessages: true,
    allowExampleUrls: true,
    txServer: nil
  })
end

Parameters:

  • definition (Hash) (defaults to: nil)

    raw fields to set, optional



111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 111

def validation_context(definition = nil, &)
  if @validation_context
    if definition
      @validation_context.definition.merge!(definition.deep_symbolize_keys)
    elsif block_given?
      @validation_context.instance_eval(&)
    end
  else
    @validation_context = ValidationContext.new(definition || {}, &)
  end
  @validation_context
end

#validator_error_message(error) ⇒ String

Add a specific error message for specific network problems to help the user

Parameters:

  • error (Exception)

    An error exception that happened during evaluator connection

Returns:

  • (String)

    A readable error message describing the specific network problem



356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 356

def validator_error_message(error)
  case error
  when Faraday::ConnectionFailed
    "Connection failed to validator at #{url}."
  when Faraday::TimeoutError
    "Timeout while connecting to validator at #{url}."
  when Faraday::SSLError
    "SSL error connecting to validator at #{url}."
  when Faraday::ClientError  # these are 400s
    "Client error (4xx) connecting to validator at #{url}."
  when Faraday::ServerError  # these are 500s
    "Server error (5xx) from validator at #{url}."
  else
    "Unable to connect to validator at #{url}."
  end
end

#validator_session_repoObject



58
59
60
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 58

def validator_session_repo
  @validator_session_repo ||= Inferno::Repositories::ValidatorSessions.new
end

#wrap_resource_for_hl7_wrapper(resource, profile_url) ⇒ Object



267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/inferno/dsl/fhir_resource_validation.rb', line 267

def wrap_resource_for_hl7_wrapper(resource, profile_url)
  validator_session_id =
    validator_session_repo.find_validator_session_id(test_suite_id,
                                                     name.to_s, requirements)

  @session_id = validator_session_id if validator_session_id

  # HL7 Validator Core 6.5.19+ renamed `cliContext` to `validationContext`.
  # This allows backward compatibility until the validator-wrapper is updated.
  context_key = Feature.use_validation_context_key? ? :validationContext : :cliContext

  wrapped_resource = {
    context_key => {
      **validation_context.definition,
      profiles: [profile_url]
    },
    filesToValidate: [
      {
        fileName: "#{resource.resourceType}/#{resource.id}.json",
        fileContent: resource.source_contents,
        fileType: 'json'
      }
    ],
    sessionId: @session_id
  }
  wrapped_resource.to_json
end