class FHIR::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]
copyright[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_models/fhir_ext/structure_definition.rb, line 28 def self.clear_all_validates_vs @vs_validators = {} end
clear_validates_vs(valueset_uri)
click to toggle source
# File lib/fhir_models/fhir_ext/structure_definition.rb, line 24 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_models/fhir_ext/structure_definition.rb, line 20 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_models/fhir_ext/structure_definition_compare.rb, line 12 def compatible?(another_definition) @errors = [] @warnings = [] @finding = FHIR::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::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::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 > 0 @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 > 0 @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_models/fhir_ext/structure_definition.rb, line 312 def data_type?(data_type_code, value) # FHIR models covers any base Resources if FHIR::RESOURCES.include?(data_type_code) definition = FHIR::Definitions.resource_definition(data_type_code) unless definition.nil? ret_val = false begin # klass = Module.const_get("FHIR::#{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::Definitions.resource_definition(resource_type) if !definition.nil? ret_val = false begin # klass = Module.const_get("FHIR::#{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::PRIMITIVES.keys.map(&:downcase) FHIR.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::Definitions.type_definition(data_type_code) definition = FHIR::Definitions.resource_definition(data_type_code) if definition.nil? if !definition.nil? ret_val = false begin # klass = Module.const_get("FHIR::#{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_models/fhir_ext/structure_definition.rb, line 91 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_models/fhir_ext/structure_definition_compare.rb, line 142 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_models/fhir_ext/structure_definition_compare.rb, line 146 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_models/fhir/resources/StructureDefinition.rb, line 162 def resourceType 'StructureDefinition' end
some_type_of_xml_or_json?(code)
click to toggle source
# File lib/fhir_models/fhir_ext/structure_definition.rb, line 436 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_models/fhir_ext/structure_definition.rb, line 36 def validate_resource(resource) @errors = [] @warnings = [] if resource.is_a?(FHIR::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_models/fhir_ext/structure_definition.rb, line 47 def validates_hash?(hash) @errors = [] @warnings = [] valid_json?(hash) if hash @errors end
validates_resource?(resource)
click to toggle source
# File lib/fhir_models/fhir_ext/structure_definition.rb, line 32 def validates_resource?(resource) validate_resource(resource).empty? end
verify_cardinality(element, nodes)
click to toggle source
# File lib/fhir_models/fhir_ext/structure_definition.rb, line 299 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_models/fhir_ext/structure_definition.rb, line 306 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_models/fhir_ext/structure_definition_compare.rb, line 160 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::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::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::ElementDefinition.from_fhir_json(e.to_fhir_json) } FHIR::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_models/fhir_ext/structure_definition.rb, line 76 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_models/fhir_ext/structure_definition.rb, line 388 def check_binding_element(element, value) vs_uri = element.binding.valueSet if vs_uri.include?('|') x = vs_uri.index('|') vs_uri = vs_uri[0..x - 1] end valueset = FHIR::Definitions.get_codes(vs_uri) matching_type = 0 if vs_uri == 'http://hl7.org/fhir/ValueSet/mimetypes' || vs_uri == 'http://www.rfc-editor.org/bcp/bcp13.txt' matches = MIME::Types[value] known_weird = ['text/cql', 'application/cql+text', 'application/hl7-v2'].include?(value) if (matches.nil? || matches.size.zero? || known_weird) && !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_models/fhir_ext/structure_definition_compare.rb, line 239 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.valueSet || y.binding.description @warnings << @finding.warning(x.path.to_s, 'binding', 'Inconsistent binding', '', val) elsif !x.binding.nil? && y.binding.nil? val = x.binding.valueSet || 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.valueSet y_vs = y.binding.valueSet 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_models/fhir_ext/structure_definition_compare.rb, line 213 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::Definitions.get_extension_definition(x.type[0].profile) y_extension = FHIR::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_models/fhir_ext/structure_definition.rb, line 99 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_models/fhir_ext/structure_definition.rb, line 56 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_models/fhir_ext/structure_definition.rb, line 126 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::CodeableConcept) 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::Definitions.get_extension_definition(value['url']) # if extension_def # verified_extension = extension_def.validates_resource?(FHIR::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::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 %w[CodeableConcept Coding Quantity].include? data_type_found required_strength = element&.binding&.strength == 'required' binding_issues = required_strength ? @errors : @warnings valueset_uri = element&.binding&.valueSet if valueset_uri&.include?('|') x = valueset_uri.index('|') valueset_uri = valueset_uri[0..x - 1] end check_code = lambda do |coding| # Can't validate if both code and system are not given if coding['code'].nil? || coding['system'].nil? @warnings << "#{describe_element(element)} code: #{coding.to_json} missing code" if coding['code'].nil? @warnings << "#{describe_element(element)} code: #{coding.to_json} missing system" if coding['system'].nil? return end # ValueSet Validation check_fn = self.class.vs_validators[valueset_uri] has_valid_code = false if check_fn has_valid_code = check_fn.call(coding) binding_issues << "#{describe_element(element)} has no codings from #{valueset_uri}. Codings evaluated: #{coding.to_json}" unless has_valid_code end # CodeSystem Validation unless has_valid_code check_fn = self.class.vs_validators[coding['system']] if check_fn && !check_fn.call(coding) binding_issues << "#{describe_element(element)} has no codings from it's specified system: #{coding['system']}. "\ "Codings evaluated: #{coding.to_json}" end end end if data_type_found == 'CodeableConcept' value['coding']&.each do |coding| check_code.call(coding) end else # avoid checking Codings twice if they are already checked as part of a CodeableConcept # The CodeableConcept should contain the binding for the children Codings check_code.call(value) unless element.path == 'CodeableConcept.coding' 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 FHIRPath invariants 'constraint.xpath' constraints... # This code is not very robust, and is likely to be throwing *many* exceptions. # This is partially because the FHIRPath 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| begin result = FHIRPath.evaluate(constraint.expression, node) if !result && constraint.severity == 'error' @errors << "#{describe_element(element)}: FHIRPath expression evaluates to false for #{name} (containing: #{node}) invariant rule #{constraint.key}: #{constraint.human}" end rescue @warnings << "#{describe_element(element)}: unable to evaluate FHIRPath expression against JSON for #{name} (containing: #{node}) invariant rule #{constraint.key}: #{constraint.human}" end 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