class FHIR::STU3::StructureDefinition

Extend StructureDefinition for profile comparison code

Constants

METADATA
SEARCH_PARAMS

Attributes

vs_validators[RW]
abstract[RW]
baseDefinition[RW]
contact[RW]
contained[RW]
context[RW]
contextInvariant[RW]
contextType[RW]
date[RW]
derivation[RW]
description[RW]
differential[RW]
errors[RW]
experimental[RW]
extension[RW]
fhirVersion[RW]
finding[RW]
hierarchy[RW]
id[RW]
identifier[RW]
implicitRules[RW]
jurisdiction[RW]
keyword[RW]
kind[RW]
language[RW]
mapping[RW]
meta[RW]
modifierExtension[RW]
name[RW]
publisher[RW]
purpose[RW]
snapshot[RW]
status[RW]
text[RW]
title[RW]
type[RW]
url[RW]
useContext[RW]
version[RW]
warnings[RW]

Public Class Methods

clear_all_validates_vs() click to toggle source
# File lib/fhir_stu3_models/fhir_ext/structure_definition.rb, line 29
def self.clear_all_validates_vs
  @vs_validators = {}
end
clear_validates_vs(valueset_uri) click to toggle source
# File lib/fhir_stu3_models/fhir_ext/structure_definition.rb, line 25
def self.clear_validates_vs(valueset_uri)
  @vs_validators.delete valueset_uri
end
validates_vs(valueset_uri, &validator_fn) click to toggle source
# File lib/fhir_stu3_models/fhir_ext/structure_definition.rb, line 21
def self.validates_vs(valueset_uri, &validator_fn)
  @vs_validators[valueset_uri] = validator_fn
end

Public Instance Methods

compatible?(another_definition) click to toggle source

Checks whether or not “another_definition” is compatible with this definition. If they have conflicting elements, restrictions, bindings, modifying extensions, etc.

# File lib/fhir_stu3_models/fhir_ext/structure_definition_compare.rb, line 13
def compatible?(another_definition)
  @errors = []
  @warnings = []

  @finding = FHIR::STU3::StructureDefinitionFinding.new
  @finding.resourceType = snapshot.element[0].path
  @finding.profileIdA = id
  @finding.profileIdB = another_definition.id if another_definition.respond_to?(:id)

  if !(another_definition.is_a? FHIR::STU3::StructureDefinition)
    @errors << @finding.error('', '', 'Not a StructureDefinition', 'StructureDefinition', another_definition.class.name.to_s)
    return false
  elsif another_definition.snapshot.element[0].path != snapshot.element[0].path
    @errors << @finding.error('', '', 'Incompatible resourceType', @finding.resourceType, another_definition.snapshot.element[0].path.to_s)
    return false
  end

  left_elements = Array.new(snapshot.element)
  right_elements = Array.new(another_definition.snapshot.element)

  left_paths = left_elements.map(&:path)
  right_paths = right_elements.map(&:path)

  # StructureDefinitions don't always include all base attributes (for example, of a ContactPoint)
  # if nothing is modified from the base definition, so we have to add them in if they are missing.
  base_definition = FHIR::STU3::Definitions.get_resource_definition(snapshot.element[0].path)
  base_elements = base_definition.snapshot.element

  left_missing = right_paths - left_paths
  # left_missing_roots = left_missing.map{|e| e.split('.')[0..-2].join('.') }.uniq
  add_missing_elements(id, left_missing, left_elements, base_elements)

  right_missing = left_paths - right_paths
  # right_missing_roots = right_missing.map{|e| e.split('.')[0..-2].join('.') }.uniq
  add_missing_elements(another_definition.id, right_missing, right_elements, base_elements)

  # update paths
  left_paths = left_elements.map(&:path)
  right_paths = right_elements.map(&:path)

  # recalculate the missing attributes
  left_missing = right_paths - left_paths
  right_missing = left_paths - right_paths

  # generate warnings for missing fields (ignoring extensions)
  left_missing.each do |e|
    next if e.include? 'extension'
    elem = get_element_by_path(e, right_elements)
    if !elem.min.nil? && elem.min.positive?
      @errors << @finding.error(e, 'min', 'Missing REQUIRED element', 'Missing', elem.min.to_s)
    elsif elem.isModifier == true
      @errors << @finding.error(e, 'isModifier', 'Missing MODIFIER element', 'Missing', elem.isModifier.to_s)
    else
      @warnings << @finding.warning(e, '', 'Missing element', 'Missing', 'Defined')
    end
  end
  right_missing.each do |e|
    next if e.include? 'extension'
    elem = get_element_by_path(e, left_elements)
    if !elem.min.nil? && elem.min.positive?
      @errors << @finding.error(e, 'min', 'Missing REQUIRED element', elem.min.to_s, 'Missing')
    elsif elem.isModifier == true
      @errors << @finding.error(e, 'isModifier', 'Missing MODIFIER element', elem.isModifier.to_s, 'Missing')
    else
      @warnings << @finding.warning(e, '', 'Missing element', 'Defined', 'Missing')
    end
  end

  left_extensions = []
  right_extensions = []

  # compare elements, starting with the elements in this definition
  left_elements.each do |x|
    if x.path.include? 'extension'
      # handle extensions separately
      left_extensions << x
    else
      y = get_element_by_path(x.path, right_elements)
      compare_element_definitions(x, y, another_definition)
    end
  end

  # now compare elements defined in the other definition, if we haven't already looked at them
  right_elements.each do |y|
    if y.path.include? 'extension'
      # handle extensions separately
      right_extensions << y
    elsif left_missing.include? y.path
      x = get_element_by_path(y.path, left_elements)
      compare_element_definitions(x, y, another_definition)
    end
  end

  # finally, compare the extensions.
  checked_extensions = []
  left_extensions.each do |x|
    y = get_extension(x.name, right_extensions)
    unless y.nil?
      # both profiles share an extension with the same name
      checked_extensions << x.name
      compare_extension_definition(x, y, another_definition)
    end
    y = get_extension(x.type[0].profile, right_extensions)
    next unless !y.nil? && x.name != y.name
    # both profiles share the same extension definition but with a different name
    checked_extensions << x.name
    checked_extensions << y.name
    compare_element_definitions(x, y, another_definition)
  end
  right_extensions.each do |y|
    next if checked_extensions.include?(y.name)
    x = get_extension(y.name, left_extensions)
    unless x.nil?
      # both profiles share an extension with the same name
      checked_extensions << y.name
      compare_extension_definition(x, y, another_definition)
    end
    x = get_extension(y.type[0].profile, left_extensions)
    next unless !x.nil? && x.name != y.name && !checked_extensions.include?(x.name)
    # both profiles share the same extension definition but with a different name
    checked_extensions << x.name
    checked_extensions << y.name
    compare_element_definitions(x, y, another_definition)
  end
  @errors.flatten!
  @warnings.flatten!
  @errors.size.zero?
end
data_type?(data_type_code, value) click to toggle source

data_type_code == a FHIR DataType code (see hl7.org/fhir/2015May/datatypes.html) value == the representation of the value

# File lib/fhir_stu3_models/fhir_ext/structure_definition.rb, line 299
def data_type?(data_type_code, value)
  # FHIR models covers any base Resources
  if FHIR::STU3::RESOURCES.include?(data_type_code)
    definition = FHIR::STU3::Definitions.resource_definition(data_type_code)
    unless definition.nil?
      ret_val = false
      begin
        # klass = Module.const_get("FHIR::STU3::#{data_type_code}")
        # ret_val = definition.validates_resource?(klass.new(deep_copy(value)))
        ret_val = definition.validates_hash?(value)
        unless ret_val
          @errors += definition.errors
          @warnings += definition.warnings
        end
      rescue
        @errors << "Unable to verify #{data_type_code} as a FHIR Resource."
      end
      return ret_val
    end
  end

  # Remaining data types: handle special cases before checking type StructureDefinitions
  case data_type_code.downcase
  when 'domainresource'
    true # we don't have to verify domain resource, because it will be included in the snapshot
  when 'resource'
    resource_type = value['resourceType']
    definition = FHIR::STU3::Definitions.resource_definition(resource_type)
    if !definition.nil?
      ret_val = false
      begin
        # klass = Module.const_get("FHIR::STU3::#{resource_type}")
        # ret_val = definition.validates_resource?(klass.new(deep_copy(value)))
        ret_val = definition.validates_hash?(value)
        unless ret_val
          @errors += definition.errors
          @warnings += definition.warnings
        end
      rescue
        @errors << "Unable to verify #{resource_type} as a FHIR Resource."
      end
      ret_val
    else
      @errors << "Unable to find base Resource definition: #{resource_type}"
      false
    end
  when *FHIR::STU3::PRIMITIVES.keys.map(&:downcase)
    FHIR::STU3.primitive?(datatype: data_type_code, value: value)
  else
    # Eliminate endless loop on Element is an Element
    return true if data_type_code == 'Element' && id == 'Element'

    definition = FHIR::STU3::Definitions.type_definition(data_type_code)
    definition = FHIR::STU3::Definitions.resource_definition(data_type_code) if definition.nil?
    if !definition.nil?
      ret_val = false
      begin
        # klass = Module.const_get("FHIR::STU3::#{data_type_code}")
        # ret_val = definition.validates_resource?(klass.new(deep_copy(value)))
        ret_val = definition.validates_hash?(value)
        unless ret_val
          @errors += definition.errors
          @warnings += definition.warnings
        end
      rescue
        @errors << "Unable to verify #{data_type_code} as a FHIR type."
      end
      ret_val
    else
      @errors << "Unable to find base type definition: #{data_type_code}"
      false
    end
  end
end
describe_element(element) click to toggle source
# File lib/fhir_stu3_models/fhir_ext/structure_definition.rb, line 92
def describe_element(element)
  if element.path.end_with?('.extension', '.modifierExtension') && element.sliceName
    "#{element.path} (#{element.sliceName})"
  else
    element.path
  end
end
get_element_by_path(path, elements = snapshot.element) click to toggle source
# File lib/fhir_stu3_models/fhir_ext/structure_definition_compare.rb, line 143
def get_element_by_path(path, elements = snapshot.element)
  elements.detect { |element| element.path == path }
end
get_extension(extension, elements = snapshot.element) click to toggle source
# File lib/fhir_stu3_models/fhir_ext/structure_definition_compare.rb, line 147
def get_extension(extension, elements = snapshot.element)
  elements.each do |element|
    if element.path.include?('extension') || element.type.map(&:code).include?('Extension')
      return element if element.name == extension || element.type.map(&:profile).include?(extension)
    end
  end
  nil
end
resourceType() click to toggle source
# File lib/fhir_stu3_models/fhir/resources/StructureDefinition.rb, line 145
def resourceType
  'StructureDefinition'
end
some_type_of_xml_or_json?(code) click to toggle source
# File lib/fhir_stu3_models/fhir_ext/structure_definition.rb, line 418
def some_type_of_xml_or_json?(code)
  m = code.downcase
  return true if m == 'xml' || m == 'json'
  return true if m.start_with?('application/', 'text/') && m.end_with?('json', 'xml')
  return true if m.start_with?('application/xml', 'text/xml', 'application/json', 'text/json')
  false
end
validate_resource(resource) click to toggle source
# File lib/fhir_stu3_models/fhir_ext/structure_definition.rb, line 37
def validate_resource(resource)
  @errors = []
  @warnings = []
  if resource.is_a?(FHIR::STU3::Model)
    valid_json?(resource.to_json) if resource
  else
    @errors << "#{resource.class} is not a resource."
  end
  @errors
end
validates_hash?(hash) click to toggle source
# File lib/fhir_stu3_models/fhir_ext/structure_definition.rb, line 48
def validates_hash?(hash)
  @errors = []
  @warnings = []
  valid_json?(hash) if hash
  @errors
end
validates_resource?(resource) click to toggle source
# File lib/fhir_stu3_models/fhir_ext/structure_definition.rb, line 33
def validates_resource?(resource)
  validate_resource(resource).empty?
end
verify_cardinality(element, nodes) click to toggle source
# File lib/fhir_stu3_models/fhir_ext/structure_definition.rb, line 286
def verify_cardinality(element, nodes)
  # Check the cardinality
  min = element.min
  max = element.max == '*' ? Float::INFINITY : element.max.to_i
  @errors << "#{describe_element(element)} failed cardinality test (#{min}..#{max}) -- found #{nodes.size}" if (nodes.size < min) || (nodes.size > max)
end
verify_fixed_value(element, value) click to toggle source
# File lib/fhir_stu3_models/fhir_ext/structure_definition.rb, line 293
def verify_fixed_value(element, value)
  @errors << "#{describe_element(element)} value of '#{value}' did not match fixed value: #{element.fixed}" if !element.fixed.nil? && element.fixed != value
end

Private Instance Methods

add_missing_elements(_name, missing_paths, elements, base_elements) click to toggle source

private name – name of the profile we're fixing missing_paths – list of paths that we're adding elements – list of elements currently defined in the profile base_elements – list of elements defined in the base resource the profile extends

# File lib/fhir_stu3_models/fhir_ext/structure_definition_compare.rb, line 161
def add_missing_elements(_name, missing_paths, elements, base_elements)
  variable_paths = elements.map(&:path).grep(/\[x\]/).map { |e| e[0..-4] }
  variable_paths << base_elements.map(&:path).grep(/\[x\]/).map { |e| e[0..-4] }
  variable_paths.flatten!.uniq!

  missing_paths.each do |path|
    # Skip extensions
    next if path.include? 'extension'

    # Skip the variable paths that end with "[x]"
    next if variable_paths.any? { |variable| path.starts_with?(variable) }

    elem = get_element_by_path(path, base_elements)
    unless elem.nil?
      # _DEEP_ copy
      elements << FHIR::STU3::ElementDefinition.from_fhir_json(elem.to_fhir_json)
      next
    end

    x = path.split('.')
    root = x.first(x.size - 1).join('.')
    next unless root.include? '.'
    # get the root element to fill in the details
    elem = get_element_by_path(root, elements)
    # get the data type definition to fill in the details
    # assume missing elements are from first data type (gross)
    next if elem.type.nil? || elem.type.empty?
    type_def = FHIR::STU3::Definitions.type_definition(elem.type[0].code)
    next if type_def.nil?
    type_elements = Array.new(type_def.snapshot.element)
    # _DEEP_ copy
    type_elements.map! do |e| # {|e| FHIR::STU3::ElementDefinition.from_fhir_json(e.to_fhir_json) }
      FHIR::STU3::ElementDefinition.from_fhir_json(e.to_fhir_json)
    end
    # Fix path names
    type_root = String.new(type_elements[0].path)
    type_elements.each { |e| e.path.gsub!(type_root, root) }
    # finally, add the missing element definitions
    # one by one -- only if they are not already present (i.e. do not override)
    type_elements.each do |z|
      y = get_element_by_path(z.path, elements)
      next unless y.nil?
      elements << z
      # else
      #   @warnings << "StructureDefinition #{name} already contains #{z.path}"
    end
    elements.uniq!
    # else
    #   @warnings << "StructureDefinition #{name} missing -- #{path}"
  end
end
build_hierarchy() click to toggle source
# File lib/fhir_stu3_models/fhir_ext/structure_definition.rb, line 77
def build_hierarchy
  @hierarchy = nil
  snapshot.element.each do |element|
    if @hierarchy.nil?
      @hierarchy = element
    else
      @hierarchy.add_descendent(element)
    end
  end
  changelist = differential.element.map(&:path)
  @hierarchy.keep_children(changelist)
  @hierarchy.sweep_children
  @hierarchy
end
check_binding_element(element, value) click to toggle source
# File lib/fhir_stu3_models/fhir_ext/structure_definition.rb, line 375
def check_binding_element(element, value)
  vs_uri = element.binding.valueSetUri || element.binding.valueSetReference.reference
  valueset = FHIR::STU3::Definitions.get_codes(vs_uri)

  matching_type = 0

  if vs_uri == 'http://hl7.org/fhir/ValueSet/content-type' || vs_uri == 'http://www.rfc-editor.org/bcp/bcp13.txt'
    matches = MIME::Types[value]
    if (matches.nil? || matches.size.zero?) && !some_type_of_xml_or_json?(value)
      @errors << "#{element.path} has invalid mime-type: '#{value}'"
      matching_type -= 1 if element.binding.strength == 'required'
    end
  elsif vs_uri == 'http://hl7.org/fhir/ValueSet/languages' || vs_uri == 'http://tools.ietf.org/html/bcp47'
    has_region = !(value =~ /-/).nil?
    valid = !BCP47::Language.identify(value.downcase).nil? && (!has_region || !BCP47::Region.identify(value.upcase).nil?)
    unless valid
      @errors << "#{element.path} has unrecognized language: '#{value}'"
      matching_type -= 1 if element.binding.strength == 'required'
    end
  elsif valueset.nil?
    @warnings << "#{element.path} has unknown ValueSet: '#{vs_uri}'"
    if element.binding.strength == 'required'
      if element.short
        @warnings << "#{element.path} guessing codes for ValueSet: '#{vs_uri}'"
        guess_codes = element.short.split(' | ')
        matching_type -= 1 unless guess_codes.include?(value)
      else
        matching_type -= 1
      end
    end
  elsif !valueset.values.flatten.include?(value)
    message = "#{element.path} has invalid code '#{value}' from #{vs_uri}"
    if element.binding.strength == 'required'
      @errors << message
      matching_type -= 1
    else
      @warnings << message
    end
  end

  matching_type
end
compare_element_definitions(x, y, another_definition) click to toggle source

private

# File lib/fhir_stu3_models/fhir_ext/structure_definition_compare.rb, line 240
def compare_element_definitions(x, y, another_definition)
  return if x.nil? || y.nil? || another_definition.nil?

  # check cardinality
  x_min = x.min || 0
  x_max = x.max == '*' ? Float::INFINITY : x.max.to_i
  y_min = y.min || 0
  y_max = y.max == '*' ? Float::INFINITY : y.max.to_i

  if x_min.nil? || x.max.nil? || y_min.nil? || y.max.nil?
    @errors << @finding.error(x.path.to_s, 'min/max', 'Unknown cardinality', "#{x_min}..#{x.max}", "#{y_min}..#{y.max}")
  elsif (x_min > y_max) || (x_max < y_min)
    @errors << @finding.error(x.path.to_s, 'min/max', 'Incompatible cardinality', "#{x_min}..#{x.max}", "#{y_min}..#{y.max}")
  elsif (x_min != y_min) || (x_max != y_max)
    @warnings << @finding.warning(x.path.to_s, 'min/max', 'Inconsistent cardinality', "#{x_min}..#{x.max}", "#{y_min}..#{y.max}")
  end

  # check data types
  x_types = x.type.map(&:code)
  y_types = y.type.map(&:code)
  x_only = x_types - y_types
  y_only = y_types - x_types
  shared = x_types - x_only

  if !shared.nil? && shared.size.zero? && !x_types.empty? && !y_types.empty? && !x.constraint.empty? && !y.constraint.empty?
    @errors << @finding.error(x.path.to_s, 'type.code', 'Incompatible data types', x_types.to_s, y_types.to_s)
  end
  if !x_only.nil? && !x_only.empty?
    @warnings << @finding.warning(x.path.to_s, 'type.code', 'Allows additional data types', x_only.to_s, 'not allowed')
  end
  if !y_only.nil? && !y_only.empty?
    @warnings << @finding.warning(x.path.to_s, 'type.code', 'Allows additional data types', 'not allowed', y_only.to_s)
  end

  # check bindings
  if x.binding.nil? && !y.binding.nil?
    val = y.binding.valueSetUri || y.binding.valueSetReference.try(:reference) || y.binding.description
    @warnings << @finding.warning(x.path.to_s, 'binding', 'Inconsistent binding', '', val)
  elsif !x.binding.nil? && y.binding.nil?
    val = x.binding.valueSetUri || x.binding.valueSetReference.try(:reference) || x.binding.description
    @warnings << @finding.warning(x.path.to_s, 'binding', 'Inconsistent binding', val, '')
  elsif !x.binding.nil? && !y.binding.nil?
    x_vs = x.binding.valueSetUri || x.binding.valueSetReference.try(:reference)
    y_vs = y.binding.valueSetUri || y.binding.valueSetReference.try(:reference)
    if x_vs != y_vs
      if x.binding.strength == 'required' || y.binding.strength == 'required'
        @errors << @finding.error(x.path.to_s, 'binding.strength', 'Incompatible bindings', "#{x.binding.strength} #{x_vs}", "#{y.binding.strength} #{y_vs}")
      else
        @warnings << @finding.warning(x.path.to_s, 'binding.strength', 'Inconsistent bindings', "#{x.binding.strength} #{x_vs}", "#{y.binding.strength} #{y_vs}")
      end
    end
  end

  # check default values
  if x.defaultValue.try(:type) != y.defaultValue.try(:type)
    @errors << @finding.error(x.path.to_s, 'defaultValue', 'Incompatible default type', x.defaultValue.try(:type).to_s, y.defaultValue.try(:type).to_s)
  end
  if x.defaultValue.try(:value) != y.defaultValue.try(:value)
    @errors << @finding.error(x.path.to_s, 'defaultValue', 'Incompatible default value', x.defaultValue.try(:value).to_s, y.defaultValue.try(:value).to_s)
  end

  # check meaning when missing
  if x.meaningWhenMissing != y.meaningWhenMissing
    @errors << @finding.error(x.path.to_s, 'meaningWhenMissing', 'Inconsistent missing meaning', x.meaningWhenMissing.tr(',', ';').to_s, y.meaningWhenMissing.tr(',', ';').to_s)
  end

  # check fixed values
  if x.fixed.try(:type) != y.fixed.try(:type)
    @errors << @finding.error(x.path.to_s, 'fixed', 'Incompatible fixed type', x.fixed.try(:type).to_s, y.fixed.try(:type).to_s)
  end
  if x.fixed != y.fixed
    xfv = x.fixed.try(:value)
    xfv = xfv.to_xml.delete(/\n/) if x.fixed.try(:value).methods.include?(:to_xml)
    yfv = y.fixed.try(:value)
    yfv = yfv.to_xml.delete(/\n/) if y.fixed.try(:value).methods.include?(:to_xml)
    @errors << @finding.error(x.path.to_s, 'fixed', 'Incompatible fixed value', xfv.to_s, yfv.to_s)
  end

  # check min values
  if x.min.try(:type) != y.min.try(:type)
    @errors << @finding.error(x.path.to_s, 'min', 'Incompatible min type', x.min.try(:type).to_s, y.min.try(:type).to_s)
  end
  if x.min.try(:value) != y.min.try(:value)
    @errors << @finding.error(x.path.to_s, 'min', 'Incompatible min value', x.min.try(:value).to_s, y.min.try(:value).to_s)
  end

  # check max values
  if x.max.try(:type) != y.max.try(:type)
    @errors << @finding.error(x.path.to_s, 'max', 'Incompatible max type', x.max.try(:type).to_s, y.max.try(:type).to_s)
  end
  if x.max.try(:value) != y.max.try(:value)
    @errors << @finding.error(x.path.to_s, 'max', 'Incompatible max value', x.max.try(:value).to_s, y.max.try(:value).to_s)
  end

  # check pattern values
  if x.pattern.try(:type) != y.pattern.try(:type)
    @errors << @finding.error(x.path.to_s, 'pattern', 'Incompatible pattern type', x.pattern.try(:type).to_s, y.pattern.try(:type).to_s)
  end
  if x.pattern.try(:value) != y.pattern.try(:value)
    @errors << @finding.error(x.path.to_s, 'pattern', 'Incompatible pattern value', x.pattern.try(:value).to_s, y.pattern.try(:value).to_s)
  end

  # maxLength (for Strings)
  if x.maxLength != y.maxLength
    @warnings << @finding.warning(x.path.to_s, 'maxLength', 'Inconsistent maximum length', x.maxLength.to_s, y.maxLength.to_s)
  end

  # constraints
  x_constraints = x.constraint.map(&:xpath)
  y_constraints = y.constraint.map(&:xpath)
  x_only = x_constraints - y_constraints
  y_only = y_constraints - x_constraints
  shared = x_constraints - x_only

  if !shared.nil? && shared.size.zero? && !x.constraint.empty? && !y.constraint.empty?
    @errors << @finding.error(x.path.to_s, 'constraint.xpath', 'Incompatible constraints', x_constraints.map { |z| z.tr(',', ';') }.join(' && ').to_s, y_constraints.map { |z| z.tr(',', ';') }.join(' && ').to_s)
  end
  if !x_only.nil? && !x_only.empty?
    @errors << @finding.error(x.path.to_s, 'constraint.xpath', 'Additional constraints', x_constraints.map { |z| z.tr(',', ';') }.join(' && ').to_s, '')
  end
  if !y_only.nil? && !y_only.empty?
    @errors << @finding.error(x.path.to_s, 'constraint.xpath', 'Additional constraints', '', y_constraints.map { |z| z.tr(',', ';') }.join(' && ').to_s)
  end

  # mustSupports
  if x.mustSupport != y.mustSupport
    @warnings << @finding.warning(x.path.to_s, 'mustSupport', 'Inconsistent mustSupport', (x.mustSupport || false).to_s, (y.mustSupport || false).to_s)
  end

  # isModifier
  return unless x.isModifier != y.isModifier
  @errors << @finding.error(x.path.to_s, 'isModifier', 'Incompatible isModifier', (x.isModifier || false).to_s, (y.isModifier || false).to_s)
end
compare_extension_definition(x, y, another_definition) click to toggle source

private

# File lib/fhir_stu3_models/fhir_ext/structure_definition_compare.rb, line 214
def compare_extension_definition(x, y, another_definition)
  x_profiles = x.type.map(&:profile)
  y_profiles = y.type.map(&:profile)
  x_only = x_profiles - y_profiles
  shared = x_profiles - x_only

  if !shared.nil? && shared.size.zero?
    # same name, but different profiles
    # maybe the profiles are the same, just with different URLs...
    # ... so we have to compare them, if we can.
    @warnings << @finding.warning("#{x.path} (#{x.name})", 'type.profile', 'Different Profiles', x_profiles.to_s, y_profiles.to_s)
    x_extension = FHIR::STU3::Definitions.get_extension_definition(x.type[0].profile)
    y_extension = FHIR::STU3::Definitions.get_extension_definition(y.type[0].profile)
    if !x_extension.nil? && !y_extension.nil?
      x_extension.compatible?(y_extension)
      @errors << x_extension.errors
      @warnings << x_extension.warnings
    else
      @warnings << @finding.warning("#{x.path} (#{x.name})", '', 'Could not find extension definitions to compare.', '', '')
    end
  else
    compare_element_definitions(x, y, another_definition)
  end
end
get_json_nodes(json, path) click to toggle source
# File lib/fhir_stu3_models/fhir_ext/structure_definition.rb, line 100
def get_json_nodes(json, path)
  results = []
  return [json] if path.nil?
  steps = path.split('.')
  steps.each.with_index do |step, index|
    if json.is_a? Hash
      json = json[step]
    elsif json.is_a? Array
      json.each do |e|
        results << get_json_nodes(e, steps[index..-1].join('.'))
      end
      return results.flatten!
    else
      # this thing doesn't exist
      return results
    end
    return results if json.nil?
  end

  if json.is_a? Array
    results += json
  else
    results << json
  end
  results
end
valid_json?(json) click to toggle source

Checks whether or not the “json” is valid according to this definition. json == the raw json for a FHIR resource

# File lib/fhir_stu3_models/fhir_ext/structure_definition.rb, line 57
def valid_json?(json)
  build_hierarchy if @hierarchy.nil?

  if json.is_a? String
    begin
      json = JSON.parse(json)
    rescue => e
      @errors << "Failed to parse JSON: #{e.message} %n #{h} %n #{e.backtrace.join("\n")}"
      return false
    end
  end

  @hierarchy.children.each do |element|
    verify_element(element, json)
  end

  @errors.size.zero?
end
verify_element(element, json) click to toggle source
# File lib/fhir_stu3_models/fhir_ext/structure_definition.rb, line 127
def verify_element(element, json)
  path = element.local_name || element.path
  path = path[(@hierarchy.path.size + 1)..-1] if path.start_with? @hierarchy.path

  if element.type && !element.type.empty?
    data_type_found = element.type.first.code
  else
    @warnings << "Unable to guess data type for #{describe_element(element)}"
    data_type_found = nil
  end

  # get the JSON nodes associated with this element path
  if path.end_with?('[x]')
    nodes = []
    element.type.each do |type|
      data_type_found = type.code
      capcode = type.code.clone
      capcode[0] = capcode[0].upcase
      nodes = get_json_nodes(json, path.gsub('[x]', capcode))
      break unless nodes.empty?
    end
  else
    nodes = get_json_nodes(json, path)
  end

  # special filtering on extension urls
  extension_profile = element.type.find { |t| t.code == 'Extension' && !t.profile.nil? }
  if extension_profile
    nodes = nodes.select { |x| extension_profile.profile == x['url'] }
  end

  verify_cardinality(element, nodes)

  return if nodes.empty?
  # Check the datatype for each node, only if the element has one declared, and it isn't the root element
  if !element.type.empty? && element.path != id
    # element.type not being empty implies data_type_found != nil, for valid profiles
    codeable_concept_pattern = element.pattern && element.pattern.is_a?(FHIR::STU3::CodeableConcept)
    codeable_concept_binding = element.binding
    matching_pattern = false
    nodes.each do |value|
      matching_type = 0

      # the element is valid, if it matches at least one of the datatypes
      temp_messages = []
      verified_extension = false
      verified_data_type = false
      if data_type_found == 'Extension' # && !type.profile.nil?
        verified_extension = true
        # TODO: should verify extensions
        # extension_def = FHIR::STU3::Definitions.get_extension_definition(value['url'])
        # if extension_def
        #   verified_extension = extension_def.validates_resource?(FHIR::STU3::Extension.new(deep_copy(value)))
        # end
      else
        temp = @errors
        @errors = []
        verified_data_type = data_type?(data_type_found, value)
        temp_messages += @errors
        @errors = temp
      end
      if data_type_found && (verified_extension || verified_data_type)
        matching_type += 1
        if data_type_found == 'code' # then check the binding
          unless element.binding.nil?
            matching_type += check_binding_element(element, value)
          end
        elsif data_type_found == 'CodeableConcept' && codeable_concept_pattern
          vcc = FHIR::STU3::CodeableConcept.new(value)
          pattern = element.pattern.coding
          pattern.each do |pcoding|
            vcc.coding.each do |vcoding|
              matching_pattern = true if vcoding.system == pcoding.system && vcoding.code == pcoding.code
            end
          end
        elsif data_type_found == 'CodeableConcept' && codeable_concept_binding
          binding_issues =
            if element.binding.strength == 'extensible'
              @warnings
            elsif element.binding.strength == 'required'
              @errors
            else # e.g., example-strength or unspecified
              [] # Drop issues errors on the floor, in throwaway array
            end

          valueset_uri = element.binding && element.binding.valueSetReference && element.binding.valueSetReference.reference
          vcc = FHIR::STU3::CodeableConcept.new(value)
          if valueset_uri && self.class.vs_validators[valueset_uri]
            check_fn = self.class.vs_validators[valueset_uri]
            has_valid_code = vcc.coding && vcc.coding.any? { |c| check_fn.call(c) }
            unless has_valid_code
              binding_issues << "#{describe_element(element)} has no codings from #{valueset_uri}. Codings evaluated: #{vcc.to_json}"
            end
          end

          unless has_valid_code
            vcc.coding.each do |c|
              check_fn = self.class.vs_validators[c.system]
              if check_fn && !check_fn.call(c)
                binding_issues << "#{describe_element(element)} has no codings from it's specified system: #{c.system}.  "\
                                  "Codings evaluated: #{vcc.to_json}"
              end
            end
          end

        elsif data_type_found == 'String' && !element.maxLength.nil? && (value.size > element.maxLength)
          @errors << "#{describe_element(element)} exceed maximum length of #{element.maxLength}: #{value}"
        end
      elsif data_type_found
        temp_messages << "#{describe_element(element)} is not a valid #{data_type_found}: '#{value}'"
      else
        # we don't know the data type... so we say "OK"
        matching_type += 1
        @warnings >> "Unable to guess data type for #{describe_element(element)}"
      end

      if matching_type <= 0
        @errors += temp_messages
        @errors << "#{describe_element(element)} did not match one of the valid data types: #{element.type.map(&:code)}"
      else
        @warnings += temp_messages
      end
      verify_fixed_value(element, value)
    end
    if codeable_concept_pattern && matching_pattern == false
      @errors << "#{describe_element(element)} CodeableConcept did not match defined pattern: #{element.pattern.to_hash}"
    end
  end

  # Check FluentPath invariants 'constraint.xpath' constraints...
  # This code is not very robust, and is likely to be throwing *many* exceptions.
  # This is partially because the FluentPath evaluator is not complete, and partially
  # because the context of an expression (element.constraint.expression) is not always
  # consistent with the current context (element.path). For example, sometimes expressions appear to be
  # written to be evaluated within the element, other times at the resource level, or perhaps
  # elsewhere. There is no good way to determine "where" you should evaluate the expression.
  element.constraint.each do |constraint|
    next unless constraint.expression && !nodes.empty?
    nodes.each do |node|
      result = FluentPath::STU3.evaluate(constraint.expression, node)
      if !result && constraint.severity == 'error'
        @errors << "#{describe_element(element)}: FluentPath expression evaluates to false for #{name} invariant rule #{constraint.key}: #{constraint.human}"
        @errors << node.to_s
      end
    rescue
      @warnings << "#{describe_element(element)}: unable to evaluate FluentPath expression against JSON for #{name} invariant rule #{constraint.key}: #{constraint.human}"
      @warnings << node.to_s
    end
  end

  # check children if the element has any
  return unless element.children
  nodes.each do |node|
    element.children.each do |child|
      verify_element(child, node)
    end
  end
end