From 1d0e5cfb28849af5d7c579aac182737ac1be745f Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Fri, 24 May 2019 16:31:59 -0400 Subject: [PATCH 01/80] breaking out validation --- .gitignore | 6 + lib/fhir_models.rb | 5 + .../validation/element_validator.rb | 41 + .../validation/profile_validator.rb | 255 ++ .../validation/validation_result.rb | 21 + lib/fhir_models/validation/validator.rb | 43 + .../validation/value_set_validator.rb | 2 + .../invalid-Patient-example.json | 134 + .../StructureDefinition-us-core-patient.json | 3825 ++++++++++++++++- test/unit/validator_test.rb | 90 + 10 files changed, 4421 insertions(+), 1 deletion(-) create mode 100644 lib/fhir_models/validation/element_validator.rb create mode 100644 lib/fhir_models/validation/profile_validator.rb create mode 100644 lib/fhir_models/validation/validation_result.rb create mode 100644 lib/fhir_models/validation/validator.rb create mode 100644 lib/fhir_models/validation/value_set_validator.rb create mode 100644 test/fixtures/invalid_resources/invalid-Patient-example.json create mode 100644 test/unit/validator_test.rb diff --git a/.gitignore b/.gitignore index cafbb7956..40896dadb 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,9 @@ tmp/ # User-specific .idea + +## Documentation cache and generated files: +/.yardoc/ +/_yardoc/ +/doc/ +/rdoc/ \ No newline at end of file diff --git a/lib/fhir_models.rb b/lib/fhir_models.rb index c9315c604..d462e45aa 100644 --- a/lib/fhir_models.rb +++ b/lib/fhir_models.rb @@ -33,3 +33,8 @@ Dir.glob(File.join(root, 'lib', 'fhir_models', 'fhir_ext', '*.rb')).each do |file| require file end + +# Require validation code +Dir.glob(File.join(root, 'lib', 'fhir_models', 'validation', '*.rb')).each do |file| + require file +end diff --git a/lib/fhir_models/validation/element_validator.rb b/lib/fhir_models/validation/element_validator.rb new file mode 100644 index 000000000..6f68eb86c --- /dev/null +++ b/lib/fhir_models/validation/element_validator.rb @@ -0,0 +1,41 @@ +module FHIR + # Element Validators accept an ElementDefinition and the associated element of the resource being validated. + module ElementValidator + # Verify that the element meets the cardinality requirements + # + # @param element [Object] The Element of the Resource under test + # @param element_definition [FHIR::ElementDefinition] The Element Definition from which the cardinality is taken + # @return result [FHIR::ValidationResult] The result of the cardinality check + def self.verify_element_cardinality(element, element_definition, current_path, skip = false) + min = element_definition.min + max = element_definition.max == '*' ? Float::INFINITY : element_definition.max.to_i + result = FHIR::ValidationResult.new + # Save a reference to the ElementDefinition + result.element_definition = element_definition + result.validation_type = :cardinality + + # Cardinality has no meaning on the first element. + # It is listed as optional(irrelevant) + # Specification Reference: http://www.hl7.org/fhir/elementdefinition.html#interpretation + # Zulip Chat: https://chat.fhir.org/#narrow/stream/179166-implementers/topic/cardinality.20of.20root.20elements/near/154024550 + if skip || !current_path.include?('.') + result.is_successful = :skipped + result.element_path = current_path || element_definition.path + return result + end + result.element_path = current_path + result.element = element + element_array = [element].flatten.compact # flatten + result.is_successful = !((element_array.size < min) || (element_array.size > max)) + result + end + + def self.verify_data_type(element, element_definition, current_path, skip = false) + element_definition.type.code.each do |datatype| + if FHIR::RESOURCES.include? datatype + + end + end + end + end +end \ No newline at end of file diff --git a/lib/fhir_models/validation/profile_validator.rb b/lib/fhir_models/validation/profile_validator.rb new file mode 100644 index 000000000..23d85d00e --- /dev/null +++ b/lib/fhir_models/validation/profile_validator.rb @@ -0,0 +1,255 @@ +module FHIR + # Validator which allows validation of a resource against a profile + class ProfileValidator + @vs_validators = {} + attr_accessor :all_results + attr_accessor :show_skipped + + # Create a ProfileValidator from a FHIR::StructureDefinition + # + # @param profile [FHIR::StructureDefinition] + # + def initialize(profile) + @profile = profile + @all_results = [] + @show_skipped = true + end + + def validate(resource) + validate_against_hierarchy(resource) + @all_results + end + + # Build a hierarchy from a list of ElementDefinitions + # + # @param [Array] List of Element Definitions + # @return [Hash] The ElementDefinition hierarchy + private def build_hierarchy(elem_def_list) + hierarchy = {} + elem_def_list.each do |element| + # Separate path and slices into an array of keys + slices = element.id.split(':') + path = slices.shift.split('.') + slices = slices.pop&.split('/') + last_path = slices.nil? ? path.pop : slices.pop + + # Build the hierarchy + thing = path.inject(hierarchy) do |memo, k| + memo[k] ||= { elementDefinition: nil, path: {}, slices: {} } + memo[k][:path] + end + + # If there are slices + unless slices.nil? + path_down = path.zip(Array.new(path.length - 1, :path)).push(:slices).flatten.compact + thing = slices.inject(hierarchy.dig(*path_down)) do |memo, k| + memo[k] ||= { elementDefinition: nil, path: {}, slices: {} } + memo[k][:slices] + end + end + + # If there are no slices + thing[last_path] = { elementDefinition: element, path: {}, slices: {} } + end + hierarchy + # traverse(hierarchy) { |x| puts x.id } + end + + # Traverse the hierarchy + # + # @param hierarchy [Hash] element definition hierarchy + # @yield [elementDefinition] provides the element definition to the block + private def traverse(hierarchy, &block) + hierarchy.each do |_, v| + # yield that element definition + yield(v[:elementDefinition]) + traverse(v[:slices], &block) unless v[:slices].empty? + traverse(v[:path], &block) unless v[:path].empty? + end + end + + private def validate_against_hierarchy(resource) + @snapshot_hierarchy ||= build_hierarchy(@profile.snapshot.element) + # Slicing is prohibited on first element so we only check the paths + # http://www.hl7.org/fhir/elementdefinition.html#interpretation + @snapshot_hierarchy.values.each do |value| + validate_hierarchy(resource, value) + end + @snapshot_hierarchy + end + + private def validate_hierarchy(resource, hierarchy, skip = false) + # Validate the element + hierarchy[:results] ||= [] + element_definition = hierarchy[:elementDefinition] + + elements = retrieve_by_element_definition(element_definition, resource, false) + result = [] + + if skip && @show_skipped + result.push(*verify_element(nil, element_definition, p, skip)) + hierarchy[:results].push(*result) + @all_results.push(*result) + return + end + + # Normalize type choice elements to just the element for cardinality testing + if element_definition.path.end_with? '[x]' + mtelms = Hash.new([]) + elements.each do |k, v| + renorm = k.rpartition('.').first + mtelms["#{renorm}.#{element_definition.path.split('.').last}"].push(v) + end + elements = mtelms + end + + # Validate the Element + elements.each do |p, el| + result.push(*verify_element(el, element_definition, p, skip)) + end + + # Save the validation results + hierarchy[:results].push(*result) + @all_results.push(*result) + + # Check to see if there are any valid elements to determine if we need to check the subelements + element_exists = !blank?(elements.values.flatten.compact) + + # If the element doesn't exist we don't need to check its subelements unless we are instructed to by showskipped + return unless @show_skipped || element_exists + + # Validate the subpath elements + hierarchy[:path].values.each { |v| validate_hierarchy(resource, v, !element_exists) } + + # Validate the slices elements + hierarchy[:slices].values.each { |v| validate_hierarchy(resource, v, !element_exists) } + end + + private def blank?(obj) + obj.respond_to?(:empty?) ? obj.empty? : obj.nil? + end + + # Verify the element in the provided resource based on the provided ElementDefinition + # + # @param resource [FHIR::Model] The resource to be validated + # @param element_definition [FHIR::ElementDefinition] The ElementDefintion Resource which provides the validation criteria + private def verify_element(element, element_definition, current_path, skip = false) + # This will hold the FHIR::ValidationResults from the various checks + results = [] + results.push(ElementValidator.verify_element_cardinality(element, element_definition, current_path, skip)) + results + end + + # Splits a path into an array + private def element_path_array(element_definition) + element_definition.path.split('.') + end + + # Retrieves all the elements associated with the path + # i.e. Patient.contact.name will return an array with all the names. + # + # @param path [String] the path + # @param resource [FHIR::Model] the resource from which the elements will be retrieved + # @return [Array] The desired elements + private def retrieve_element(path, resource) + path.split('.').drop(1).inject(resource) do |memo, meth| + [memo].flatten.map { |thing| thing.send(meth) } + end + end + + # Retrieves all the elements associated with the given path with the FHIRPath of the element + # + # + # @param path [String] the path + # @param resource [FHIR::Model] the resource from which the elements will be retrieved + # @return [Hash] The desired elements + private def retrieve_element_with_fhirpath(path, resource, indexed = true) + spath = path.split('.') + base = spath.shift + fhirpath_elements = { base => resource } + last = spath.pop unless indexed + + desired_elements = spath.inject(fhirpath_elements) do |memo, meth| + digging = {} + memo.each do |k, v| + elms = v.send(meth) + # More than one element where the FHIRPath needs indexing + if elms.respond_to? :each_with_index + elms.each_with_index do |vv,kk| + digging["#{k}.#{meth}[#{kk}]"] = vv unless blank?(vv) + end + # Just One + else + digging["#{k}.#{meth}"] = elms unless blank?(elms) + end + end + digging + end + + return desired_elements unless last + + # If we don't want to index the last element (useful for testing cardinality) + not_indexed = {} + desired_elements.each do |k, v| + elms = v.send(last) + not_indexed["#{k}.#{last}"] = elms + end + not_indexed + end + + private def retrieve_by_element_definition(elementdefinition, resource, indexed = true) + # Check if we were provided a path that includes extensions (like in the ElementDefinition id versus the path) + path = elementdefinition.path + + # Check for multiple choice types + if path.include? '[x]' + elements = {} + elementdefinition.type.each do |type| + choice_type = type.code[0].upcase + type.code[1..-1] + type_element = retrieve_element_with_fhirpath(path.gsub('[x]', choice_type), resource, indexed) + elements.merge!(type_element) unless blank?(type_element) + end + else + elements = retrieve_element_with_fhirpath(path, resource, indexed) + end + + # Handle Slices + if elementdefinition.sliceName + # Grab Extension slices + if elementdefinition.type.one? + if elementdefinition.type.first.code == 'Extension' + # Only select the elements which match the slice profile. + elements.select! do |k,v| + if indexed + v.url == elementdefinition.type.first.profile.first + else + v.select! {|vv| vv.url == elementdefinition.type.first.profile.first} + end + end + else + raise UnhandledSlice("Slice type #{elementdefinition.type.code} is not handled. Only Extension slices are handled") + end + else + raise UnhandledSlice("Slice has more than one type. #{elementdefinition.id}") + end + end + elements + end + + # Method for registering ValueSet and CodeSystem Validators + # + # @param valueset_uri [String] The ValueSet URI the validator is related to + # @param validator [#validate] The validator + def register_vs_validator(valueset_uri, validator) + @vs_validators[valueset_uri] = validator + end + + # This Exception is for indicating types of slices that are not handled. + # + class UnhandledSlice < StandardError + def initialize(msg="Unhandled Slice") + super(msg) + end + end + end +end diff --git a/lib/fhir_models/validation/validation_result.rb b/lib/fhir_models/validation/validation_result.rb new file mode 100644 index 000000000..a24bd8fae --- /dev/null +++ b/lib/fhir_models/validation/validation_result.rb @@ -0,0 +1,21 @@ +module FHIR + # A result return by a validator + class ValidationResult + attr_accessor :element + attr_accessor :element_definition + attr_accessor :is_successful + attr_accessor :validation_type + attr_accessor :element_definition_id + attr_accessor :element_path + + # Returns the validation result as an OperationOutcome + # + # @return [FHIR::OperationOutcome] The OperationOutcome + def to_operation_outcome; end + + # Returns the result of the validation as a Hash + # + # @param [Hash] The results of the validation + def to_hash; end + end +end diff --git a/lib/fhir_models/validation/validator.rb b/lib/fhir_models/validation/validator.rb new file mode 100644 index 000000000..c65db7284 --- /dev/null +++ b/lib/fhir_models/validation/validator.rb @@ -0,0 +1,43 @@ +module FHIR + # FHIR Resource Validator + # Implementation inspired by FHIR HAPI Validation + class Validator + attr_reader :validator_modules + attr_accessor :show_skipped + + # Creates a FHIR Validator + # + # @param validator_modules[ValidatorModule, Array, #validate] An array of validator_modules + def initialize(validator_modules = []) + @validator_modules = Set.new + add_validator_module(validator_modules) + end + + # Register a validator_module + # @param [#validate] validator_module + def register_validator_module(validator_module) + @validator_modules.each {|validator| validator.show_skipped = @show_skipped} + add_validator_module(validator_module) + end + + # @param resource [FHIR::Model] The Resource to be validated + # @return [Hash] the validation results + def validate(resource) + @validator_modules.flat_map { |validator| validator.validate(resource) } + end + + def show_skipped=(skip) + @show_skipped = skip + @validator_modules.each {|validator| validator.show_skipped = skip} + end + + # Helper method for adding validator modules + # + # This allows an individual validator to be passed or an array of validators + # + # @param validators [#validate] The validators to be added + private def add_validator_module(validators) + @validator_modules.merge([validators].flatten) + end + end +end diff --git a/lib/fhir_models/validation/value_set_validator.rb b/lib/fhir_models/validation/value_set_validator.rb new file mode 100644 index 000000000..5ae4bc8d0 --- /dev/null +++ b/lib/fhir_models/validation/value_set_validator.rb @@ -0,0 +1,2 @@ +class ValueSetValidator +end diff --git a/test/fixtures/invalid_resources/invalid-Patient-example.json b/test/fixtures/invalid_resources/invalid-Patient-example.json new file mode 100644 index 000000000..1101a4f58 --- /dev/null +++ b/test/fixtures/invalid_resources/invalid-Patient-example.json @@ -0,0 +1,134 @@ +{ + "resourceType": "Patient", + "id": "example", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient" + ] + }, + "text": { + "status": "generated", + "div": "
\n\t\t\t

\n\t\t\t\tGenerated Narrative with Details\n\t\t\t

\n\t\t\t

\n\t\t\t\tid: example

\n\t\t\t

\n\t\t\t\tidentifier: Medical Record Number = 1032702 (USUAL)

\n\t\t\t

\n\t\t\t\tactive: true

\n\t\t\t

\n\t\t\t\tname: Amy V. Shaw

\n\t\t\t

\n\t\t\t\ttelecom: ph: 555-555-5555(HOME), amy.shaw@example.com

\n\t\t\t

\n\t\t\t\tgender:

\n\t\t\t

\n\t\t\t\tbirthsex: Female

\n\t\t\t

\n\t\t\t\tbirthDate: Feb 20, 2007

\n\t\t\t

\n\t\t\t\taddress: 49 Meadow St Mounds OK 74047 US

\n\t\t\t

\n\t\t\t\trace: White, American Indian or Alaska Native, Asian, Shoshone, Filipino

\n\t\t\t

\n\t\t\t\tethnicity: Hispanic or Latino, Dominican, Mexican

\n\t\t
" + }, + "extension": [ + { + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", + "extension": [ + { + "url": "ombCategory", + "valueCoding": { + "system": "urn:oid:2.16.840.1.113883.6.238", + "code": "2106-3", + "display": "White" + } + }, + { + "url": "ombCategory", + "valueCoding": { + "system": "urn:oid:2.16.840.1.113883.6.238", + "code": "1002-5", + "display": "American Indian or Alaska Native" + } + }, + { + "url": "ombCategory", + "valueCoding": { + "system": "urn:oid:2.16.840.1.113883.6.238", + "code": "2028-9", + "display": "Asian" + } + }, + { + "url": "detailed", + "valueCoding": { + "system": "urn:oid:2.16.840.1.113883.6.238", + "code": "1586-7", + "display": "Shoshone" + } + }, + { + "url": "detailed", + "valueCoding": { + "system": "urn:oid:2.16.840.1.113883.6.238", + "code": "2036-2", + "display": "Filipino" + } + }, + { + "url": "text", + "valueString": "Mixed" + } + ] + }, + { + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity", + "extension": [ + { + "url": "ombCategory", + "valueCoding": { + "system": "urn:oid:2.16.840.1.113883.6.238", + "code": "2135-2", + "display": "Hispanic or Latino" + } + }, + { + "url": "detailed", + "valueCoding": { + "system": "urn:oid:2.16.840.1.113883.6.238", + "code": "2184-0", + "display": "Dominican" + } + }, + { + "url": "detailed", + "valueCoding": { + "system": "urn:oid:2.16.840.1.113883.6.238", + "code": "2148-5", + "display": "Mexican" + } + }, + { + "url": "text", + "valueString": "Hispanic or Latino" + } + ] + }, + { + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex", + "valueCode": "F" + } + ], + "telecom": [ + { + "system": "phone", + "value": "555-555-5555", + "use": "home" + }, + { + "system": "email", + "value": "amy.shaw@example.com" + } + ], + "gender": "female", + "maritalStatus": { + "coding": [ + { + "system": "http://hl7.org/fhir/v3/MaritalStatus", + "code": "BADCODE" + } + ], + "text": "S" + }, + "birthDate": "2007-02-20", + "address": [ + { + "line": [ + "49 Meadow St" + ], + "city": "Mounds", + "state": "OK", + "postalCode": "74047", + "country": "US" + } + ] +} \ No newline at end of file diff --git a/test/fixtures/us_core/StructureDefinition-us-core-patient.json b/test/fixtures/us_core/StructureDefinition-us-core-patient.json index 3e27914be..efb184e52 100644 --- a/test/fixtures/us_core/StructureDefinition-us-core-patient.json +++ b/test/fixtures/us_core/StructureDefinition-us-core-patient.json @@ -1 +1,3824 @@ -{"resourceType":"StructureDefinition","id":"us-core-patient","text":{"status":"generated","div":"
\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n
NameFlagsCard.TypeDescription & Constraints\"doco\"
\".\"\".\" Patient 0..*US Core Patient Profile
\".\"\".\"\".\" us-core-race S0..1(Complex)US Core Race Extension
URL: http://hl7.org/fhir/us/core/StructureDefinition/us-core-race
\".\"\".\"\".\" us-core-ethnicity S0..1(Complex)US Core ethnicity Extension
URL: http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity
\".\"\".\"\".\" us-core-birthsex S0..1codeExtension
URL: http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex
Binding: Birth Sex (required)
\".\"\".\"\".\" identifier S1..*Identifier
\".\"\".\"\".\"\".\" system S1..1uri
\".\"\".\"\".\"\".\" value S1..1stringThe value that is unique within the system.
\".\"\".\"\".\" name S1..*HumanName
\".\"\".\"\".\"\".\" family S1..1string
\".\"\".\"\".\"\".\" given S1..*string
\".\"\".\"\".\" gender S1..1codeBinding: AdministrativeGender (required)
\".\"\".\"\".\" birthDate S0..1date
\".\"\".\"\".\" communication S0..*BackboneElement
\".\"\".\"\".\"\".\" language S1..1CodeableConceptBinding: Language codes with language and optionally a region modifier (extensible)
Max Binding: Language codes with language and optionally a region modifier

\"doco\" Documentation for this format
"},"url":"http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient","version":"4.0.0","name":"US Core Patient Profile","title":"US Core Patient Profile","status":"active","date":"2016-08-01T00:00:00+00:00","publisher":"HL7 US Realm Steering Committee","contact":[{"telecom":[{"system":"url","value":"http://www.healthit.gov"}]}],"description":"Defines constraints and extensions on the patient resource for the minimal set of data to query and retrieve patient demographic information.","jurisdiction":[{"coding":[{"system":"urn:iso:std:iso:3166","code":"US","display":"United States of America"}]}],"fhirVersion":"4.0.0","mapping":[{"identity":"argonaut-dq-dstu2","uri":"http://unknown.org/Argonaut-DQ-DSTU2","name":"Argonaut-DQ-DSTU2"},{"identity":"rim","uri":"http://hl7.org/v3","name":"RIM Mapping"},{"identity":"cda","uri":"http://hl7.org/v3/cda","name":"CDA (R2)"},{"identity":"w5","uri":"http://hl7.org/fhir/fivews","name":"FiveWs Pattern Mapping"},{"identity":"v2","uri":"http://hl7.org/v2","name":"HL7 v2 Mapping"},{"identity":"loinc","uri":"http://loinc.org","name":"LOINC code for the element"}],"kind":"resource","abstract":false,"type":"Patient","baseDefinition":"http://hl7.org/fhir/StructureDefinition/Patient","derivation":"constraint","snapshot":{"element":[{"id":"Patient","path":"Patient","short":"US Core Patient Profile","definition":"The US Core Patient Profile is based upon the core FHIR Patient Resource and designed to meet the applicable patient demographic data elements from the 2015 Edition Common Clinical Data Set.","alias":["SubjectOfCare Client Resident"],"min":0,"max":"*","base":{"path":"Patient","min":0,"max":"*"},"constraint":[{"key":"dom-2","severity":"error","human":"If the resource is contained in another resource, it SHALL NOT contain nested Resources","expression":"contained.contained.empty()","xpath":"not(parent::f:contained and f:contained)","source":"DomainResource"},{"key":"dom-4","severity":"error","human":"If a resource is contained in another resource, it SHALL NOT have a meta.versionId or a meta.lastUpdated","expression":"contained.meta.versionId.empty() and contained.meta.lastUpdated.empty()","xpath":"not(exists(f:contained/*/f:meta/f:versionId)) and not(exists(f:contained/*/f:meta/f:lastUpdated))","source":"DomainResource"},{"key":"dom-3","severity":"error","human":"If the resource is contained in another resource, it SHALL be referred to from elsewhere in the resource or SHALL refer to the containing resource","expression":"contained.where((('#'+id in (%resource.descendants().reference | %resource.descendants().as(canonical) | %resource.descendants().as(uri) | %resource.descendants().as(url))) or descendants().where(reference = '#').exists() or descendants().where(as(canonical) = '#').exists() or descendants().where(as(canonical) = '#').exists()).not()).trace('unmatched', id).empty()","xpath":"not(exists(for $contained in f:contained return $contained[not(parent::*/descendant::f:reference/@value=concat('#', $contained/*/id/@value) or descendant::f:reference[@value='#'])]))","source":"DomainResource"},{"extension":[{"url":"http://hl7.org/fhir/StructureDefinition/elementdefinition-bestpractice","valueBoolean":true},{"url":"http://hl7.org/fhir/StructureDefinition/elementdefinition-bestpractice-explanation","valueMarkdown":"When a resource has no narrative, only systems that fully understand the data can display the resource to a human safely. Including a human readable representation in the resource makes for a much more robust eco-system and cheaper handling of resources by intermediary systems. Some ecosystems restrict distribution of resources to only those systems that do fully understand the resources, and as a consequence implementers may believe that the narrative is superfluous. However experience shows that such eco-systems often open up to new participants over time."}],"key":"dom-6","severity":"warning","human":"A resource should have narrative for robust management","expression":"text.div.exists()","xpath":"exists(f:text/h:div)","source":"DomainResource"},{"key":"dom-5","severity":"error","human":"If a resource is contained in another resource, it SHALL NOT have a security label","expression":"contained.meta.security.empty()","xpath":"not(exists(f:contained/*/f:meta/f:security))","source":"DomainResource"}],"mustSupport":false,"isModifier":false,"isSummary":false,"mapping":[{"identity":"rim","map":"Entity. Role, or Act"},{"identity":"rim","map":"Patient[classCode=PAT]"},{"identity":"cda","map":"ClinicalDocument.recordTarget.patientRole"},{"identity":"argonaut-dq-dstu2","map":"Patient"}]},{"id":"Patient.id","path":"Patient.id","short":"Logical id of this artifact","definition":"The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes.","comment":"The only time that a resource does not have an id is when it is being submitted to the server using a create operation.","min":0,"max":"1","base":{"path":"Resource.id","min":0,"max":"1"},"type":[{"code":"id"}],"isModifier":false,"isSummary":true},{"id":"Patient.meta","path":"Patient.meta","short":"Metadata about the resource","definition":"The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource.","min":0,"max":"1","base":{"path":"Resource.meta","min":0,"max":"1"},"type":[{"code":"Meta"}],"isModifier":false,"isSummary":true},{"id":"Patient.implicitRules","path":"Patient.implicitRules","short":"A set of rules under which this content was created","definition":"A reference to a set of rules that were followed when the resource was constructed, and which must be understood when processing the content. Often, this is a reference to an implementation guide that defines the special rules along with other profiles etc.","comment":"Asserting this rule set restricts the content to be only understood by a limited set of trading partners. This inherently limits the usefulness of the data in the long term. However, the existing health eco-system is highly fractured, and not yet ready to define, collect, and exchange data in a generally computable sense. Wherever possible, implementers and/or specification writers should avoid using this element. Often, when used, the URL is a reference to an implementation guide that defines these special rules as part of it's narrative along with other profiles, value sets, etc.","min":0,"max":"1","base":{"path":"Resource.implicitRules","min":0,"max":"1"},"type":[{"code":"uri"}],"isModifier":true,"isModifierReason":"This element is labeled as a modifier because the implicit rules may provide additional knowledge about the resource that modifies it's meaning or interpretation","isSummary":true},{"id":"Patient.language","path":"Patient.language","short":"Language of the resource content","definition":"The base language in which the resource is written.","comment":"Language is provided to support indexing and accessibility (typically, services such as text to speech use the language tag). The html language tag in the narrative applies to the narrative. The language tag on the resource may be used to specify the language of other presentations generated from the data in the resource. Not all the content has to be in the base language. The Resource.language should not be assumed to apply to the narrative automatically. If a language is specified, it should it also be specified on the div element in the html (see rules in HTML5 for information about the relationship between xml:lang and the html lang attribute).","min":0,"max":"1","base":{"path":"Resource.language","min":0,"max":"1"},"type":[{"code":"code"}],"isModifier":false,"isSummary":false,"binding":{"extension":[{"url":"http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet","valueCanonical":"http://hl7.org/fhir/ValueSet/all-languages"},{"url":"http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName","valueString":"Language"},{"url":"http://hl7.org/fhir/StructureDefinition/elementdefinition-isCommonBinding","valueBoolean":true}],"strength":"preferred","description":"A human language.","valueSet":"http://hl7.org/fhir/ValueSet/languages"}},{"id":"Patient.text","path":"Patient.text","short":"Text summary of the resource, for human interpretation","definition":"A human-readable narrative that contains a summary of the resource and can be used to represent the content of the resource to a human. The narrative need not encode all the structured data, but is required to contain sufficient detail to make it \"clinically safe\" for a human to just read the narrative. Resource definitions may define what content should be represented in the narrative to ensure clinical safety.","comment":"Contained resources do not have narrative. Resources that are not contained SHOULD have a narrative. In some cases, a resource may only have text with little or no additional discrete data (as long as all minOccurs=1 elements are satisfied). This may be necessary for data from legacy systems where information is captured as a \"text blob\" or where text is additionally entered raw or narrated and encoded information is added later.","alias":["narrative","html","xhtml","display"],"min":0,"max":"1","base":{"path":"DomainResource.text","min":0,"max":"1"},"type":[{"code":"Narrative"}],"isModifier":false,"isSummary":false,"mapping":[{"identity":"rim","map":"Act.text?"}]},{"id":"Patient.contained","path":"Patient.contained","short":"Contained, inline Resources","definition":"These resources do not have an independent existence apart from the resource that contains them - they cannot be identified independently, and nor can they have their own independent transaction scope.","comment":"This should never be done when the content can be identified properly, as once identification is lost, it is extremely difficult (and context dependent) to restore it again. Contained resources may have profiles and tags In their meta elements, but SHALL NOT have security labels.","alias":["inline resources","anonymous resources","contained resources"],"min":0,"max":"*","base":{"path":"DomainResource.contained","min":0,"max":"*"},"type":[{"code":"Resource"}],"isModifier":false,"isSummary":false,"mapping":[{"identity":"rim","map":"N/A"}]},{"id":"Patient.extension","path":"Patient.extension","slicing":{"id":"1","discriminator":[{"type":"value","path":"url"}],"ordered":false,"rules":"open"},"short":"Extension","definition":"An Extension","min":0,"max":"*","base":{"path":"DomainResource.extension","min":0,"max":"*"},"type":[{"code":"Extension"}],"isModifier":false,"isSummary":false},{"id":"Patient.extension:race","path":"Patient.extension","sliceName":"race","short":"Extension","definition":"An Extension","min":0,"max":"1","base":{"path":"DomainResource.extension","min":0,"max":"*"},"type":[{"code":"Extension","profile":["http://hl7.org/fhir/us/core/StructureDefinition/us-core-race"]}],"mustSupport":true,"isModifier":false,"isSummary":false,"mapping":[{"identity":"argonaut-dq-dstu2","map":"Patient.extension"}]},{"id":"Patient.extension:ethnicity","path":"Patient.extension","sliceName":"ethnicity","short":"Extension","definition":"An Extension","min":0,"max":"1","base":{"path":"DomainResource.extension","min":0,"max":"*"},"type":[{"code":"Extension","profile":["http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity"]}],"mustSupport":true,"isModifier":false,"isSummary":false,"mapping":[{"identity":"argonaut-dq-dstu2","map":"Patient.extension"}]},{"id":"Patient.extension:birthsex","path":"Patient.extension","sliceName":"birthsex","short":"Extension","definition":"An Extension","min":0,"max":"1","base":{"path":"DomainResource.extension","min":0,"max":"*"},"type":[{"code":"Extension","profile":["http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex"]}],"mustSupport":true,"isModifier":false,"isSummary":false,"mapping":[{"identity":"argonaut-dq-dstu2","map":"Patient.extension"}]},{"id":"Patient.modifierExtension","path":"Patient.modifierExtension","short":"Extensions that cannot be ignored","definition":"May be used to represent additional information that is not part of the basic definition of the resource and that modifies the understanding of the element that contains it and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer is allowed to define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself).","comment":"There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.","requirements":"Modifier extensions allow for extensions that *cannot* be safely ignored to be clearly distinguished from the vast majority of extensions which can be safely ignored. This promotes interoperability by eliminating the need for implementers to prohibit the presence of extensions. For further information, see the [definition of modifier extensions](http://build.fhir.org/extensibility.html#modifierExtension).","alias":["extensions","user content"],"min":0,"max":"*","base":{"path":"DomainResource.modifierExtension","min":0,"max":"*"},"type":[{"code":"Extension"}],"isModifier":true,"isModifierReason":"Modifier extensions are expected to modify the meaning or interpretation of the resource that contains them","isSummary":false,"mapping":[{"identity":"rim","map":"N/A"}]},{"id":"Patient.identifier","path":"Patient.identifier","short":"An identifier for this patient","definition":"An identifier for this patient.","requirements":"Patients are almost always assigned specific numerical identifiers.","min":1,"max":"*","base":{"path":"Patient.identifier","min":0,"max":"*"},"type":[{"code":"Identifier"}],"mustSupport":true,"isModifier":false,"isSummary":true,"mapping":[{"identity":"w5","map":"FiveWs.identifier"},{"identity":"v2","map":"PID-3"},{"identity":"rim","map":"id"},{"identity":"cda","map":".id"},{"identity":"argonaut-dq-dstu2","map":"Patient.identifier"}]},{"id":"Patient.identifier.id","path":"Patient.identifier.id","representation":["xmlAttr"],"short":"Unique id for inter-element referencing","definition":"Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.","min":0,"max":"1","base":{"path":"Element.id","min":0,"max":"1"},"type":[{"code":"string"}],"isModifier":false,"isSummary":false,"mapping":[{"identity":"rim","map":"n/a"}]},{"id":"Patient.identifier.extension","path":"Patient.identifier.extension","slicing":{"discriminator":[{"type":"value","path":"url"}],"description":"Extensions are always sliced by (at least) url","rules":"open"},"short":"Additional content defined by implementations","definition":"May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.","comment":"There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.","alias":["extensions","user content"],"min":0,"max":"*","base":{"path":"Element.extension","min":0,"max":"*"},"type":[{"code":"Extension"}],"isModifier":false,"isSummary":false,"mapping":[{"identity":"rim","map":"n/a"}]},{"id":"Patient.identifier.use","path":"Patient.identifier.use","short":"usual | official | temp | secondary | old (If known)","definition":"The purpose of this identifier.","comment":"Applications can assume that an identifier is permanent unless it explicitly says that it is temporary.","requirements":"Allows the appropriate identifier for a particular context of use to be selected from among a set of identifiers.","min":0,"max":"1","base":{"path":"Identifier.use","min":0,"max":"1"},"type":[{"code":"code"}],"isModifier":true,"isModifierReason":"This is labeled as \"Is Modifier\" because applications should not mistake a temporary id for a permanent one.","isSummary":true,"binding":{"extension":[{"url":"http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName","valueString":"IdentifierUse"}],"strength":"required","description":"Identifies the purpose for this identifier, if known .","valueSet":"http://hl7.org/fhir/ValueSet/identifier-use|4.0.0"},"mapping":[{"identity":"v2","map":"N/A"},{"identity":"rim","map":"Role.code or implied by context"}]},{"id":"Patient.identifier.type","path":"Patient.identifier.type","short":"Description of identifier","definition":"A coded type for the identifier that can be used to determine which identifier to use for a specific purpose.","comment":"This element deals only with general categories of identifiers. It SHOULD not be used for codes that correspond 1..1 with the Identifier.system. Some identifiers may fall into multiple categories due to common usage. Where the system is known, a type is unnecessary because the type is always part of the system definition. However systems often need to handle identifiers where the system is not known. There is not a 1:1 relationship between type and system, since many different systems have the same type.","requirements":"Allows users to make use of identifiers when the identifier system is not known.","min":0,"max":"1","base":{"path":"Identifier.type","min":0,"max":"1"},"type":[{"code":"CodeableConcept"}],"isModifier":false,"isSummary":true,"binding":{"extension":[{"url":"http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName","valueString":"IdentifierType"},{"url":"http://hl7.org/fhir/StructureDefinition/elementdefinition-isCommonBinding","valueBoolean":true}],"strength":"extensible","description":"A coded type for an identifier that can be used to determine which identifier to use for a specific purpose.","valueSet":"http://hl7.org/fhir/ValueSet/identifier-type"},"mapping":[{"identity":"v2","map":"CX.5"},{"identity":"rim","map":"Role.code or implied by context"}]},{"id":"Patient.identifier.system","path":"Patient.identifier.system","short":"The namespace for the identifier value","definition":"Establishes the namespace for the value - that is, a URL that describes a set values that are unique.","comment":"Identifier.system is always case sensitive.","requirements":"There are many sets of identifiers. To perform matching of two identifiers, we need to know what set we're dealing with. The system identifies a particular set of unique identifiers.","min":1,"max":"1","base":{"path":"Identifier.system","min":0,"max":"1"},"type":[{"code":"uri"}],"example":[{"label":"General","valueUri":"http://www.acme.com/identifiers/patient"}],"mustSupport":true,"isModifier":false,"isSummary":true,"mapping":[{"identity":"v2","map":"CX.4 / EI-2-4"},{"identity":"rim","map":"II.root or Role.id.root"},{"identity":"servd","map":"./IdentifierType"},{"identity":"argonaut-dq-dstu2","map":"Patient.identifier.system"}]},{"id":"Patient.identifier.value","path":"Patient.identifier.value","short":"The value that is unique within the system.","definition":"The portion of the identifier typically relevant to the user and which is unique within the context of the system.","comment":"If the value is a full URI, then the system SHALL be urn:ietf:rfc:3986. The value's primary purpose is computational mapping. As a result, it may be normalized for comparison purposes (e.g. removing non-significant whitespace, dashes, etc.) A value formatted for human display can be conveyed using the [Rendered Value extension](http://build.fhir.org/extension-rendered-value.html). Identifier.value is to be treated as case sensitive unless knowledge of the Identifier.system allows the processer to be confident that non-case-sensitive processing is safe.","min":1,"max":"1","base":{"path":"Identifier.value","min":0,"max":"1"},"type":[{"code":"string"}],"example":[{"label":"General","valueString":"123456"}],"mustSupport":true,"isModifier":false,"isSummary":true,"mapping":[{"identity":"v2","map":"CX.1 / EI.1"},{"identity":"rim","map":"II.extension or II.root if system indicates OID or GUID (Or Role.id.extension or root)"},{"identity":"servd","map":"./Value"},{"identity":"argonaut-dq-dstu2","map":"Patient.identifier.value"}]},{"id":"Patient.identifier.period","path":"Patient.identifier.period","short":"Time period when id is/was valid for use","definition":"Time period during which identifier is/was valid for use.","min":0,"max":"1","base":{"path":"Identifier.period","min":0,"max":"1"},"type":[{"code":"Period"}],"isModifier":false,"isSummary":true,"mapping":[{"identity":"v2","map":"CX.7 + CX.8"},{"identity":"rim","map":"Role.effectiveTime or implied by context"},{"identity":"servd","map":"./StartDate and ./EndDate"}]},{"id":"Patient.identifier.assigner","path":"Patient.identifier.assigner","short":"Organization that issued id (may be just text)","definition":"Organization that issued/manages the identifier.","comment":"The Identifier.assigner may omit the .reference element and only contain a .display element reflecting the name or other textual information about the assigning organization.","min":0,"max":"1","base":{"path":"Identifier.assigner","min":0,"max":"1"},"type":[{"code":"Reference","targetProfile":["http://hl7.org/fhir/StructureDefinition/Organization"]}],"isModifier":false,"isSummary":true,"mapping":[{"identity":"v2","map":"CX.4 / (CX.4,CX.9,CX.10)"},{"identity":"rim","map":"II.assigningAuthorityName but note that this is an improper use by the definition of the field. Also Role.scoper"},{"identity":"servd","map":"./IdentifierIssuingAuthority"}]},{"id":"Patient.active","path":"Patient.active","short":"Whether this patient's record is in active use","definition":"Whether this patient record is in active use. \nMany systems use this property to mark as non-current patients, such as those that have not been seen for a period of time based on an organization's business rules.\n\nIt is often used to filter patient lists to exclude inactive patients\n\nDeceased patients may also be marked as inactive for the same reasons, but may be active for some time after death.","comment":"If a record is inactive, and linked to an active record, then future patient/record updates should occur on the other patient.","requirements":"Need to be able to mark a patient record as not to be used because it was created in error.","min":0,"max":"1","base":{"path":"Patient.active","min":0,"max":"1"},"type":[{"code":"boolean"}],"meaningWhenMissing":"This resource is generally assumed to be active if no value is provided for the active element","isModifier":true,"isModifierReason":"This element is labelled as a modifier because it is a status element that can indicate that a record should not be treated as valid","isSummary":true,"mapping":[{"identity":"w5","map":"FiveWs.status"},{"identity":"rim","map":"statusCode"},{"identity":"cda","map":"n/a"}]},{"id":"Patient.name","path":"Patient.name","short":"A name associated with the patient","definition":"A name associated with the individual.","comment":"A patient may have multiple names with different uses or applicable periods. For animals, the name is a \"HumanName\" in the sense that is assigned and used by humans and has the same patterns.","requirements":"Need to be able to track the patient by multiple names. Examples are your official name and a partner name.","min":1,"max":"*","base":{"path":"Patient.name","min":0,"max":"*"},"type":[{"code":"HumanName"}],"mustSupport":true,"isModifier":false,"isSummary":true,"mapping":[{"identity":"v2","map":"PID-5, PID-9"},{"identity":"rim","map":"name"},{"identity":"cda","map":".patient.name"},{"identity":"argonaut-dq-dstu2","map":"Patient.name"}]},{"id":"Patient.name.id","path":"Patient.name.id","representation":["xmlAttr"],"short":"Unique id for inter-element referencing","definition":"Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.","min":0,"max":"1","base":{"path":"Element.id","min":0,"max":"1"},"type":[{"code":"string"}],"isModifier":false,"isSummary":false,"mapping":[{"identity":"rim","map":"n/a"}]},{"id":"Patient.name.extension","path":"Patient.name.extension","slicing":{"discriminator":[{"type":"value","path":"url"}],"description":"Extensions are always sliced by (at least) url","rules":"open"},"short":"Additional content defined by implementations","definition":"May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.","comment":"There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.","alias":["extensions","user content"],"min":0,"max":"*","base":{"path":"Element.extension","min":0,"max":"*"},"type":[{"code":"Extension"}],"isModifier":false,"isSummary":false,"mapping":[{"identity":"rim","map":"n/a"}]},{"id":"Patient.name.use","path":"Patient.name.use","short":"usual | official | temp | nickname | anonymous | old | maiden","definition":"Identifies the purpose for this name.","comment":"Applications can assume that a name is current unless it explicitly says that it is temporary or old.","requirements":"Allows the appropriate name for a particular context of use to be selected from among a set of names.","min":0,"max":"1","base":{"path":"HumanName.use","min":0,"max":"1"},"type":[{"code":"code"}],"isModifier":true,"isModifierReason":"This is labeled as \"Is Modifier\" because applications should not mistake a temporary or old name etc.for a current/permanent one","isSummary":true,"binding":{"extension":[{"url":"http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName","valueString":"NameUse"}],"strength":"required","description":"The use of a human name.","valueSet":"http://hl7.org/fhir/ValueSet/name-use|4.0.0"},"mapping":[{"identity":"v2","map":"XPN.7, but often indicated by which field contains the name"},{"identity":"rim","map":"unique(./use)"},{"identity":"servd","map":"./NamePurpose"}]},{"id":"Patient.name.text","path":"Patient.name.text","short":"Text representation of the full name","definition":"Specifies the entire name as it should be displayed e.g. on an application UI. This may be provided instead of or as well as the specific parts.","comment":"Can provide both a text representation and parts. Applications updating a name SHALL ensure that when both text and parts are present, no content is included in the text that isn't found in a part.","requirements":"A renderable, unencoded form.","min":0,"max":"1","base":{"path":"HumanName.text","min":0,"max":"1"},"type":[{"code":"string"}],"isModifier":false,"isSummary":true,"mapping":[{"identity":"v2","map":"implied by XPN.11"},{"identity":"rim","map":"./formatted"}]},{"id":"Patient.name.family","path":"Patient.name.family","short":"Family name (often called 'Surname')","definition":"The part of a name that links to the genealogy. In some cultures (e.g. Eritrea) the family name of a son is the first name of his father.","comment":"Family Name may be decomposed into specific parts using extensions (de, nl, es related cultures).","alias":["surname"],"min":1,"max":"1","base":{"path":"HumanName.family","min":0,"max":"1"},"type":[{"code":"string"}],"mustSupport":true,"isModifier":false,"isSummary":true,"mapping":[{"identity":"v2","map":"XPN.1/FN.1"},{"identity":"rim","map":"./part[partType = FAM]"},{"identity":"servd","map":"./FamilyName"},{"identity":"argonaut-dq-dstu2","map":"Patient.name.family"}]},{"id":"Patient.name.given","path":"Patient.name.given","short":"Given names (not always 'first'). Includes middle names","definition":"Given name.","comment":"If only initials are recorded, they may be used in place of the full name parts. Initials may be separated into multiple given names but often aren't due to paractical limitations. This element is not called \"first name\" since given names do not always come first.","alias":["first name","middle name"],"min":1,"max":"*","base":{"path":"HumanName.given","min":0,"max":"*"},"type":[{"code":"string"}],"orderMeaning":"Given Names appear in the correct order for presenting the name","mustSupport":true,"isModifier":false,"isSummary":true,"mapping":[{"identity":"v2","map":"XPN.2 + XPN.3"},{"identity":"rim","map":"./part[partType = GIV]"},{"identity":"servd","map":"./GivenNames"},{"identity":"argonaut-dq-dstu2","map":"Patient.name.given"}]},{"id":"Patient.name.prefix","path":"Patient.name.prefix","short":"Parts that come before the name","definition":"Part of the name that is acquired as a title due to academic, legal, employment or nobility status, etc. and that appears at the start of the name.","min":0,"max":"*","base":{"path":"HumanName.prefix","min":0,"max":"*"},"type":[{"code":"string"}],"orderMeaning":"Prefixes appear in the correct order for presenting the name","isModifier":false,"isSummary":true,"mapping":[{"identity":"v2","map":"XPN.5"},{"identity":"rim","map":"./part[partType = PFX]"},{"identity":"servd","map":"./TitleCode"}]},{"id":"Patient.name.suffix","path":"Patient.name.suffix","short":"Parts that come after the name","definition":"Part of the name that is acquired as a title due to academic, legal, employment or nobility status, etc. and that appears at the end of the name.","min":0,"max":"*","base":{"path":"HumanName.suffix","min":0,"max":"*"},"type":[{"code":"string"}],"orderMeaning":"Suffixes appear in the correct order for presenting the name","isModifier":false,"isSummary":true,"mapping":[{"identity":"v2","map":"XPN/4"},{"identity":"rim","map":"./part[partType = SFX]"}]},{"id":"Patient.name.period","path":"Patient.name.period","short":"Time period when name was/is in use","definition":"Indicates the period of time when this name was valid for the named person.","requirements":"Allows names to be placed in historical context.","min":0,"max":"1","base":{"path":"HumanName.period","min":0,"max":"1"},"type":[{"code":"Period"}],"isModifier":false,"isSummary":true,"mapping":[{"identity":"v2","map":"XPN.13 + XPN.14"},{"identity":"rim","map":"./usablePeriod[type=\"IVL\"]"},{"identity":"servd","map":"./StartDate and ./EndDate"}]},{"id":"Patient.telecom","path":"Patient.telecom","short":"A contact detail for the individual","definition":"A contact detail (e.g. a telephone number or an email address) by which the individual may be contacted.","comment":"A Patient may have multiple ways to be contacted with different uses or applicable periods. May need to have options for contacting the person urgently and also to help with identification. The address might not go directly to the individual, but may reach another party that is able to proxy for the patient (i.e. home phone, or pet owner's phone).","requirements":"People have (primary) ways to contact them in some way such as phone, email.","min":0,"max":"*","base":{"path":"Patient.telecom","min":0,"max":"*"},"type":[{"code":"ContactPoint"}],"isModifier":false,"isSummary":true,"mapping":[{"identity":"v2","map":"PID-13, PID-14, PID-40"},{"identity":"rim","map":"telecom"},{"identity":"cda","map":".telecom"}]},{"id":"Patient.gender","path":"Patient.gender","short":"male | female | other | unknown","definition":"Administrative Gender - the gender that the patient is considered to have for administration and record keeping purposes.","comment":"The gender might not match the biological sex as determined by genetics or the individual's preferred identification. Note that for both humans and particularly animals, there are other legitimate possibilities than male and female, though the vast majority of systems and contexts only support male and female. Systems providing decision support or enforcing business rules should ideally do this on the basis of Observations dealing with the specific sex or gender aspect of interest (anatomical, chromosomal, social, etc.) However, because these observations are infrequently recorded, defaulting to the administrative gender is common practice. Where such defaulting occurs, rule enforcement should allow for the variation between administrative and biological, chromosomal and other gender aspects. For example, an alert about a hysterectomy on a male should be handled as a warning or overridable error, not a \"hard\" error. See the Patient Gender and Sex section for additional information about communicating patient gender and sex.","requirements":"Needed for identification of the individual, in combination with (at least) name and birth date.","min":1,"max":"1","base":{"path":"Patient.gender","min":0,"max":"1"},"type":[{"code":"code"}],"mustSupport":true,"isModifier":false,"isSummary":true,"binding":{"strength":"required","valueSet":"http://hl7.org/fhir/ValueSet/administrative-gender"},"mapping":[{"identity":"v2","map":"PID-8"},{"identity":"rim","map":"player[classCode=PSN|ANM and determinerCode=INSTANCE]/administrativeGender"},{"identity":"cda","map":".patient.administrativeGenderCode"},{"identity":"argonaut-dq-dstu2","map":"Patient.gender"}]},{"id":"Patient.birthDate","path":"Patient.birthDate","short":"The date of birth for the individual","definition":"The date of birth for the individual.","comment":"At least an estimated year should be provided as a guess if the real DOB is unknown There is a standard extension \"patient-birthTime\" available that should be used where Time is required (such as in maternity/infant care systems).","requirements":"Age of the individual drives many clinical processes.","min":0,"max":"1","base":{"path":"Patient.birthDate","min":0,"max":"1"},"type":[{"code":"date"}],"mustSupport":true,"isModifier":false,"isSummary":true,"mapping":[{"identity":"v2","map":"PID-7"},{"identity":"rim","map":"player[classCode=PSN|ANM and determinerCode=INSTANCE]/birthTime"},{"identity":"cda","map":".patient.birthTime"},{"identity":"loinc","map":"21112-8"},{"identity":"argonaut-dq-dstu2","map":"Patient.birthDate"}]},{"id":"Patient.deceased[x]","path":"Patient.deceased[x]","short":"Indicates if the individual is deceased or not","definition":"Indicates if the individual is deceased or not.","comment":"If there's no value in the instance, it means there is no statement on whether or not the individual is deceased. Most systems will interpret the absence of a value as a sign of the person being alive.","requirements":"The fact that a patient is deceased influences the clinical process. Also, in human communication and relation management it is necessary to know whether the person is alive.","min":0,"max":"1","base":{"path":"Patient.deceased[x]","min":0,"max":"1"},"type":[{"code":"boolean"},{"code":"dateTime"}],"isModifier":true,"isModifierReason":"This element is labeled as a modifier because once a patient is marked as deceased, the actions that are appropriate to perform on the patient may be significantly different.","isSummary":true,"mapping":[{"identity":"v2","map":"PID-30 (bool) and PID-29 (datetime)"},{"identity":"rim","map":"player[classCode=PSN|ANM and determinerCode=INSTANCE]/deceasedInd, player[classCode=PSN|ANM and determinerCode=INSTANCE]/deceasedTime"},{"identity":"cda","map":"n/a"}]},{"id":"Patient.address","path":"Patient.address","short":"An address for the individual","definition":"An address for the individual.","comment":"Patient may have multiple addresses with different uses or applicable periods.","requirements":"May need to keep track of patient addresses for contacting, billing or reporting requirements and also to help with identification.","min":0,"max":"*","base":{"path":"Patient.address","min":0,"max":"*"},"type":[{"code":"Address"}],"isModifier":false,"isSummary":true,"mapping":[{"identity":"v2","map":"PID-11"},{"identity":"rim","map":"addr"},{"identity":"cda","map":".addr"}]},{"id":"Patient.maritalStatus","path":"Patient.maritalStatus","short":"Marital (civil) status of a patient","definition":"This field contains a patient's most recent marital (civil) status.","requirements":"Most, if not all systems capture it.","min":0,"max":"1","base":{"path":"Patient.maritalStatus","min":0,"max":"1"},"type":[{"code":"CodeableConcept"}],"isModifier":false,"isSummary":false,"binding":{"extension":[{"url":"http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName","valueString":"MaritalStatus"},{"url":"http://hl7.org/fhir/StructureDefinition/elementdefinition-isCommonBinding","valueBoolean":true}],"strength":"extensible","description":"The domestic partnership status of a person.","valueSet":"http://hl7.org/fhir/ValueSet/marital-status"},"mapping":[{"identity":"v2","map":"PID-16"},{"identity":"rim","map":"player[classCode=PSN]/maritalStatusCode"},{"identity":"cda","map":".patient.maritalStatusCode"}]},{"id":"Patient.multipleBirth[x]","path":"Patient.multipleBirth[x]","short":"Whether patient is part of a multiple birth","definition":"Indicates whether the patient is part of a multiple (boolean) or indicates the actual birth order (integer).","comment":"Where the valueInteger is provided, the number is the birth number in the sequence. E.g. The middle birth in triplets would be valueInteger=2 and the third born would have valueInteger=3 If a boolean value was provided for this triplets example, then all 3 patient records would have valueBoolean=true (the ordering is not indicated).","requirements":"For disambiguation of multiple-birth children, especially relevant where the care provider doesn't meet the patient, such as labs.","min":0,"max":"1","base":{"path":"Patient.multipleBirth[x]","min":0,"max":"1"},"type":[{"code":"boolean"},{"code":"integer"}],"isModifier":false,"isSummary":false,"mapping":[{"identity":"v2","map":"PID-24 (bool), PID-25 (integer)"},{"identity":"rim","map":"player[classCode=PSN|ANM and determinerCode=INSTANCE]/multipleBirthInd, player[classCode=PSN|ANM and determinerCode=INSTANCE]/multipleBirthOrderNumber"},{"identity":"cda","map":"n/a"}]},{"id":"Patient.photo","path":"Patient.photo","short":"Image of the patient","definition":"Image of the patient.","comment":"Guidelines:\n* Use id photos, not clinical photos.\n* Limit dimensions to thumbnail.\n* Keep byte count low to ease resource updates.","requirements":"Many EHR systems have the capability to capture an image of the patient. Fits with newer social media usage too.","min":0,"max":"*","base":{"path":"Patient.photo","min":0,"max":"*"},"type":[{"code":"Attachment"}],"isModifier":false,"isSummary":false,"mapping":[{"identity":"v2","map":"OBX-5 - needs a profile"},{"identity":"rim","map":"player[classCode=PSN|ANM and determinerCode=INSTANCE]/desc"},{"identity":"cda","map":"n/a"}]},{"id":"Patient.contact","extension":[{"url":"http://hl7.org/fhir/StructureDefinition/structuredefinition-explicit-type-name","valueString":"Contact"}],"path":"Patient.contact","short":"A contact party (e.g. guardian, partner, friend) for the patient","definition":"A contact party (e.g. guardian, partner, friend) for the patient.","comment":"Contact covers all kinds of contact parties: family members, business contacts, guardians, caregivers. Not applicable to register pedigree and family ties beyond use of having contact.","requirements":"Need to track people you can contact about the patient.","min":0,"max":"*","base":{"path":"Patient.contact","min":0,"max":"*"},"type":[{"code":"BackboneElement"}],"constraint":[{"key":"ele-1","severity":"error","human":"All FHIR elements must have a @value or children","expression":"hasValue() or (children().count() > id.count())","xpath":"@value|f:*|h:div","source":"Element"},{"key":"pat-1","severity":"error","human":"SHALL at least contain a contact's details or a reference to an organization","expression":"name.exists() or telecom.exists() or address.exists() or organization.exists()","xpath":"exists(f:name) or exists(f:telecom) or exists(f:address) or exists(f:organization)"}],"isModifier":false,"isSummary":false,"mapping":[{"identity":"rim","map":"player[classCode=PSN|ANM and determinerCode=INSTANCE]/scopedRole[classCode=CON]"},{"identity":"cda","map":"n/a"}]},{"id":"Patient.contact.id","path":"Patient.contact.id","representation":["xmlAttr"],"short":"Unique id for inter-element referencing","definition":"Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.","min":0,"max":"1","base":{"path":"Element.id","min":0,"max":"1"},"type":[{"code":"string"}],"isModifier":false,"isSummary":false,"mapping":[{"identity":"rim","map":"n/a"}]},{"id":"Patient.contact.extension","path":"Patient.contact.extension","short":"Additional content defined by implementations","definition":"May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.","comment":"There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.","alias":["extensions","user content"],"min":0,"max":"*","base":{"path":"Element.extension","min":0,"max":"*"},"type":[{"code":"Extension"}],"isModifier":false,"isSummary":false,"mapping":[{"identity":"rim","map":"n/a"}]},{"id":"Patient.contact.modifierExtension","path":"Patient.contact.modifierExtension","short":"Extensions that cannot be ignored even if unrecognized","definition":"May be used to represent additional information that is not part of the basic definition of the element and that modifies the understanding of the element in which it is contained and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself).","comment":"There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.","requirements":"Modifier extensions allow for extensions that *cannot* be safely ignored to be clearly distinguished from the vast majority of extensions which can be safely ignored. This promotes interoperability by eliminating the need for implementers to prohibit the presence of extensions. For further information, see the [definition of modifier extensions](http://build.fhir.org/extensibility.html#modifierExtension).","alias":["extensions","user content","modifiers"],"min":0,"max":"*","base":{"path":"BackboneElement.modifierExtension","min":0,"max":"*"},"type":[{"code":"Extension"}],"isModifier":true,"isModifierReason":"Modifier extensions are expected to modify the meaning or interpretation of the element that contains them","isSummary":true,"mapping":[{"identity":"rim","map":"N/A"}]},{"id":"Patient.contact.relationship","path":"Patient.contact.relationship","short":"The kind of relationship","definition":"The nature of the relationship between the patient and the contact person.","requirements":"Used to determine which contact person is the most relevant to approach, depending on circumstances.","min":0,"max":"*","base":{"path":"Patient.contact.relationship","min":0,"max":"*"},"type":[{"code":"CodeableConcept"}],"isModifier":false,"isSummary":false,"binding":{"extension":[{"url":"http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName","valueString":"ContactRelationship"}],"strength":"extensible","description":"The nature of the relationship between a patient and a contact person for that patient.","valueSet":"http://hl7.org/fhir/ValueSet/patient-contactrelationship"},"mapping":[{"identity":"v2","map":"NK1-7, NK1-3"},{"identity":"rim","map":"code"},{"identity":"cda","map":"n/a"}]},{"id":"Patient.contact.name","path":"Patient.contact.name","short":"A name associated with the contact person","definition":"A name associated with the contact person.","requirements":"Contact persons need to be identified by name, but it is uncommon to need details about multiple other names for that contact person.","min":0,"max":"1","base":{"path":"Patient.contact.name","min":0,"max":"1"},"type":[{"code":"HumanName"}],"isModifier":false,"isSummary":false,"mapping":[{"identity":"v2","map":"NK1-2"},{"identity":"rim","map":"name"},{"identity":"cda","map":"n/a"}]},{"id":"Patient.contact.telecom","path":"Patient.contact.telecom","short":"A contact detail for the person","definition":"A contact detail for the person, e.g. a telephone number or an email address.","comment":"Contact may have multiple ways to be contacted with different uses or applicable periods. May need to have options for contacting the person urgently, and also to help with identification.","requirements":"People have (primary) ways to contact them in some way such as phone, email.","min":0,"max":"*","base":{"path":"Patient.contact.telecom","min":0,"max":"*"},"type":[{"code":"ContactPoint"}],"isModifier":false,"isSummary":false,"mapping":[{"identity":"v2","map":"NK1-5, NK1-6, NK1-40"},{"identity":"rim","map":"telecom"},{"identity":"cda","map":"n/a"}]},{"id":"Patient.contact.address","path":"Patient.contact.address","short":"Address for the contact person","definition":"Address for the contact person.","requirements":"Need to keep track where the contact person can be contacted per postal mail or visited.","min":0,"max":"1","base":{"path":"Patient.contact.address","min":0,"max":"1"},"type":[{"code":"Address"}],"isModifier":false,"isSummary":false,"mapping":[{"identity":"v2","map":"NK1-4"},{"identity":"rim","map":"addr"},{"identity":"cda","map":"n/a"}]},{"id":"Patient.contact.gender","path":"Patient.contact.gender","short":"male | female | other | unknown","definition":"Administrative Gender - the gender that the contact person is considered to have for administration and record keeping purposes.","requirements":"Needed to address the person correctly.","min":0,"max":"1","base":{"path":"Patient.contact.gender","min":0,"max":"1"},"type":[{"code":"code"}],"isModifier":false,"isSummary":false,"binding":{"extension":[{"url":"http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName","valueString":"AdministrativeGender"},{"url":"http://hl7.org/fhir/StructureDefinition/elementdefinition-isCommonBinding","valueBoolean":true}],"strength":"required","description":"The gender of a person used for administrative purposes.","valueSet":"http://hl7.org/fhir/ValueSet/administrative-gender|4.0.0"},"mapping":[{"identity":"v2","map":"NK1-15"},{"identity":"rim","map":"player[classCode=PSN|ANM and determinerCode=INSTANCE]/administrativeGender"},{"identity":"cda","map":"n/a"}]},{"id":"Patient.contact.organization","path":"Patient.contact.organization","short":"Organization that is associated with the contact","definition":"Organization on behalf of which the contact is acting or for which the contact is working.","requirements":"For guardians or business related contacts, the organization is relevant.","min":0,"max":"1","base":{"path":"Patient.contact.organization","min":0,"max":"1"},"type":[{"code":"Reference","targetProfile":["http://hl7.org/fhir/StructureDefinition/Organization"]}],"condition":["pat-1"],"isModifier":false,"isSummary":false,"mapping":[{"identity":"v2","map":"NK1-13, NK1-30, NK1-31, NK1-32, NK1-41"},{"identity":"rim","map":"scoper"},{"identity":"cda","map":"n/a"}]},{"id":"Patient.contact.period","path":"Patient.contact.period","short":"The period during which this contact person or organization is valid to be contacted relating to this patient","definition":"The period during which this contact person or organization is valid to be contacted relating to this patient.","min":0,"max":"1","base":{"path":"Patient.contact.period","min":0,"max":"1"},"type":[{"code":"Period"}],"isModifier":false,"isSummary":false,"mapping":[{"identity":"rim","map":"effectiveTime"},{"identity":"cda","map":"n/a"}]},{"id":"Patient.communication","path":"Patient.communication","short":"A language which may be used to communicate with the patient about his or her health","definition":"A language which may be used to communicate with the patient about his or her health.","comment":"If no language is specified, this *implies* that the default local language is spoken. If you need to convey proficiency for multiple modes, then you need multiple Patient.Communication associations. For animals, language is not a relevant field, and should be absent from the instance. If the Patient does not speak the default local language, then the Interpreter Required Standard can be used to explicitly declare that an interpreter is required.","requirements":"If a patient does not speak the local language, interpreters may be required, so languages spoken and proficiency are important things to keep track of both for patient and other persons of interest.","min":0,"max":"*","base":{"path":"Patient.communication","min":0,"max":"*"},"type":[{"code":"BackboneElement"}],"constraint":[{"key":"ele-1","severity":"error","human":"All FHIR elements must have a @value or children","expression":"hasValue() or (children().count() > id.count())","xpath":"@value|f:*|h:div","source":"Element"}],"mustSupport":true,"isModifier":false,"isSummary":false,"mapping":[{"identity":"rim","map":"LanguageCommunication"},{"identity":"cda","map":"patient.languageCommunication"},{"identity":"argonaut-dq-dstu2","map":"Patient.communication"}]},{"id":"Patient.communication.id","path":"Patient.communication.id","representation":["xmlAttr"],"short":"Unique id for inter-element referencing","definition":"Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.","min":0,"max":"1","base":{"path":"Element.id","min":0,"max":"1"},"type":[{"code":"string"}],"isModifier":false,"isSummary":false,"mapping":[{"identity":"rim","map":"n/a"}]},{"id":"Patient.communication.extension","path":"Patient.communication.extension","short":"Additional content defined by implementations","definition":"May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.","comment":"There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.","alias":["extensions","user content"],"min":0,"max":"*","base":{"path":"Element.extension","min":0,"max":"*"},"type":[{"code":"Extension"}],"isModifier":false,"isSummary":false,"mapping":[{"identity":"rim","map":"n/a"}]},{"id":"Patient.communication.modifierExtension","path":"Patient.communication.modifierExtension","short":"Extensions that cannot be ignored even if unrecognized","definition":"May be used to represent additional information that is not part of the basic definition of the element and that modifies the understanding of the element in which it is contained and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself).","comment":"There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.","requirements":"Modifier extensions allow for extensions that *cannot* be safely ignored to be clearly distinguished from the vast majority of extensions which can be safely ignored. This promotes interoperability by eliminating the need for implementers to prohibit the presence of extensions. For further information, see the [definition of modifier extensions](http://build.fhir.org/extensibility.html#modifierExtension).","alias":["extensions","user content","modifiers"],"min":0,"max":"*","base":{"path":"BackboneElement.modifierExtension","min":0,"max":"*"},"type":[{"code":"Extension"}],"isModifier":true,"isModifierReason":"Modifier extensions are expected to modify the meaning or interpretation of the element that contains them","isSummary":true,"mapping":[{"identity":"rim","map":"N/A"}]},{"id":"Patient.communication.language","path":"Patient.communication.language","short":"The language which can be used to communicate with the patient about his or her health","definition":"The ISO-639-1 alpha 2 code in lower case for the language, optionally followed by a hyphen and the ISO-3166-1 alpha 2 code for the region in upper case; e.g. \"en\" for English, or \"en-US\" for American English versus \"en-EN\" for England English.","comment":"The structure aa-BB with this exact casing is one the most widely used notations for locale. However not all systems actually code this but instead have it as free text. Hence CodeableConcept instead of code as the data type.","requirements":"Most systems in multilingual countries will want to convey language. Not all systems actually need the regional dialect.","min":1,"max":"1","base":{"path":"Patient.communication.language","min":1,"max":"1"},"type":[{"code":"CodeableConcept"}],"mustSupport":true,"isModifier":false,"isSummary":false,"binding":{"extension":[{"url":"http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet","valueCanonical":"http://hl7.org/fhir/us/core/ValueSet/simple-language"}],"strength":"extensible","valueSet":"http://hl7.org/fhir/us/core/ValueSet/simple-language"},"mapping":[{"identity":"v2","map":"PID-15, LAN-2"},{"identity":"rim","map":"player[classCode=PSN|ANM and determinerCode=INSTANCE]/languageCommunication/code"},{"identity":"cda","map":".languageCode"},{"identity":"argonaut-dq-dstu2","map":"Patient.communication.language"}]},{"id":"Patient.communication.preferred","path":"Patient.communication.preferred","short":"Language preference indicator","definition":"Indicates whether or not the patient prefers this language (over other languages he masters up a certain level).","comment":"This language is specifically identified for communicating healthcare information.","requirements":"People that master multiple languages up to certain level may prefer one or more, i.e. feel more confident in communicating in a particular language making other languages sort of a fall back method.","min":0,"max":"1","base":{"path":"Patient.communication.preferred","min":0,"max":"1"},"type":[{"code":"boolean"}],"isModifier":false,"isSummary":false,"mapping":[{"identity":"v2","map":"PID-15"},{"identity":"rim","map":"preferenceInd"},{"identity":"cda","map":".preferenceInd"}]},{"id":"Patient.generalPractitioner","path":"Patient.generalPractitioner","short":"Patient's nominated primary care provider","definition":"Patient's nominated care provider.","comment":"This may be the primary care provider (in a GP context), or it may be a patient nominated care manager in a community/disability setting, or even organization that will provide people to perform the care provider roles. It is not to be used to record Care Teams, these should be in a CareTeam resource that may be linked to the CarePlan or EpisodeOfCare resources.\nMultiple GPs may be recorded against the patient for various reasons, such as a student that has his home GP listed along with the GP at university during the school semesters, or a \"fly-in/fly-out\" worker that has the onsite GP also included with his home GP to remain aware of medical issues.\n\nJurisdictions may decide that they can profile this down to 1 if desired, or 1 per type.","alias":["careProvider"],"min":0,"max":"*","base":{"path":"Patient.generalPractitioner","min":0,"max":"*"},"type":[{"code":"Reference","targetProfile":["http://hl7.org/fhir/StructureDefinition/Organization","http://hl7.org/fhir/StructureDefinition/Practitioner","http://hl7.org/fhir/StructureDefinition/PractitionerRole"]}],"isModifier":false,"isSummary":false,"mapping":[{"identity":"v2","map":"PD1-4"},{"identity":"rim","map":"subjectOf.CareEvent.performer.AssignedEntity"},{"identity":"cda","map":"n/a"}]},{"id":"Patient.managingOrganization","path":"Patient.managingOrganization","short":"Organization that is the custodian of the patient record","definition":"Organization that is the custodian of the patient record.","comment":"There is only one managing organization for a specific patient record. Other organizations will have their own Patient record, and may use the Link property to join the records together (or a Person resource which can include confidence ratings for the association).","requirements":"Need to know who recognizes this patient record, manages and updates it.","min":0,"max":"1","base":{"path":"Patient.managingOrganization","min":0,"max":"1"},"type":[{"code":"Reference","targetProfile":["http://hl7.org/fhir/StructureDefinition/Organization"]}],"isModifier":false,"isSummary":true,"mapping":[{"identity":"rim","map":"scoper"},{"identity":"cda","map":".providerOrganization"}]},{"id":"Patient.link","path":"Patient.link","short":"Link to another patient resource that concerns the same actual person","definition":"Link to another patient resource that concerns the same actual patient.","comment":"There is no assumption that linked patient records have mutual links.","requirements":"There are multiple use cases: \n\n* Duplicate patient records due to the clerical errors associated with the difficulties of identifying humans consistently, and \n* Distribution of patient information across multiple servers.","min":0,"max":"*","base":{"path":"Patient.link","min":0,"max":"*"},"type":[{"code":"BackboneElement"}],"constraint":[{"key":"ele-1","severity":"error","human":"All FHIR elements must have a @value or children","expression":"hasValue() or (children().count() > id.count())","xpath":"@value|f:*|h:div","source":"Element"}],"isModifier":true,"isModifierReason":"This element is labeled as a modifier because it might not be the main Patient resource, and the referenced patient should be used instead of this Patient record. This is when the link.type value is 'replaced-by'","isSummary":true,"mapping":[{"identity":"rim","map":"outboundLink"},{"identity":"cda","map":"n/a"}]},{"id":"Patient.link.id","path":"Patient.link.id","representation":["xmlAttr"],"short":"Unique id for inter-element referencing","definition":"Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.","min":0,"max":"1","base":{"path":"Element.id","min":0,"max":"1"},"type":[{"code":"string"}],"isModifier":false,"isSummary":false,"mapping":[{"identity":"rim","map":"n/a"}]},{"id":"Patient.link.extension","path":"Patient.link.extension","short":"Additional content defined by implementations","definition":"May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.","comment":"There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.","alias":["extensions","user content"],"min":0,"max":"*","base":{"path":"Element.extension","min":0,"max":"*"},"type":[{"code":"Extension"}],"isModifier":false,"isSummary":false,"mapping":[{"identity":"rim","map":"n/a"}]},{"id":"Patient.link.modifierExtension","path":"Patient.link.modifierExtension","short":"Extensions that cannot be ignored even if unrecognized","definition":"May be used to represent additional information that is not part of the basic definition of the element and that modifies the understanding of the element in which it is contained and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself).","comment":"There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.","requirements":"Modifier extensions allow for extensions that *cannot* be safely ignored to be clearly distinguished from the vast majority of extensions which can be safely ignored. This promotes interoperability by eliminating the need for implementers to prohibit the presence of extensions. For further information, see the [definition of modifier extensions](http://build.fhir.org/extensibility.html#modifierExtension).","alias":["extensions","user content","modifiers"],"min":0,"max":"*","base":{"path":"BackboneElement.modifierExtension","min":0,"max":"*"},"type":[{"code":"Extension"}],"isModifier":true,"isModifierReason":"Modifier extensions are expected to modify the meaning or interpretation of the element that contains them","isSummary":true,"mapping":[{"identity":"rim","map":"N/A"}]},{"id":"Patient.link.other","path":"Patient.link.other","short":"The other patient or related person resource that the link refers to","definition":"The other patient resource that the link refers to.","comment":"Referencing a RelatedPerson here removes the need to use a Person record to associate a Patient and RelatedPerson as the same individual.","min":1,"max":"1","base":{"path":"Patient.link.other","min":1,"max":"1"},"type":[{"extension":[{"url":"http://hl7.org/fhir/StructureDefinition/structuredefinition-hierarchy","valueBoolean":false}],"code":"Reference","targetProfile":["http://hl7.org/fhir/StructureDefinition/Patient","http://hl7.org/fhir/StructureDefinition/RelatedPerson"]}],"isModifier":false,"isSummary":true,"mapping":[{"identity":"v2","map":"PID-3, MRG-1"},{"identity":"rim","map":"id"},{"identity":"cda","map":"n/a"}]},{"id":"Patient.link.type","path":"Patient.link.type","short":"replaced-by | replaces | refer | seealso","definition":"The type of link between this patient resource and another patient resource.","min":1,"max":"1","base":{"path":"Patient.link.type","min":1,"max":"1"},"type":[{"code":"code"}],"isModifier":false,"isSummary":true,"binding":{"extension":[{"url":"http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName","valueString":"LinkType"}],"strength":"required","description":"The type of link between this patient resource and another patient resource.","valueSet":"http://hl7.org/fhir/ValueSet/link-type|4.0.0"},"mapping":[{"identity":"rim","map":"typeCode"},{"identity":"cda","map":"n/a"}]}]},"differential":{"element":[{"id":"Patient","path":"Patient","short":"US Core Patient Profile","definition":"The US Core Patient Profile is based upon the core FHIR Patient Resource and designed to meet the applicable patient demographic data elements from the 2015 Edition Common Clinical Data Set.","mustSupport":false,"isModifier":false,"mapping":[{"identity":"argonaut-dq-dstu2","map":"Patient"}]},{"id":"Patient.extension:race","path":"Patient.extension","sliceName":"race","min":0,"max":"1","type":[{"code":"Extension","profile":["http://hl7.org/fhir/us/core/StructureDefinition/us-core-race"]}],"mustSupport":true,"isModifier":false,"mapping":[{"identity":"argonaut-dq-dstu2","map":"Patient.extension"}]},{"id":"Patient.extension:ethnicity","path":"Patient.extension","sliceName":"ethnicity","min":0,"max":"1","type":[{"code":"Extension","profile":["http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity"]}],"mustSupport":true,"isModifier":false,"mapping":[{"identity":"argonaut-dq-dstu2","map":"Patient.extension"}]},{"id":"Patient.extension:birthsex","path":"Patient.extension","sliceName":"birthsex","min":0,"max":"1","type":[{"code":"Extension","profile":["http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex"]}],"mustSupport":true,"isModifier":false,"binding":{"strength":"required","description":"Code for sex assigned at birth","valueSet":"http://hl7.org/fhir/us/core/ValueSet/us-core-birthsex"},"mapping":[{"identity":"argonaut-dq-dstu2","map":"Patient.extension"}]},{"id":"Patient.identifier","path":"Patient.identifier","min":1,"max":"*","type":[{"code":"Identifier"}],"mustSupport":true,"isModifier":false,"mapping":[{"identity":"argonaut-dq-dstu2","map":"Patient.identifier"}]},{"id":"Patient.identifier.system","path":"Patient.identifier.system","min":1,"max":"1","type":[{"code":"uri"}],"mustSupport":true,"isModifier":false,"mapping":[{"identity":"argonaut-dq-dstu2","map":"Patient.identifier.system"}]},{"id":"Patient.identifier.value","path":"Patient.identifier.value","short":"The value that is unique within the system.","min":1,"max":"1","type":[{"code":"string"}],"mustSupport":true,"isModifier":false,"mapping":[{"identity":"argonaut-dq-dstu2","map":"Patient.identifier.value"}]},{"id":"Patient.name","path":"Patient.name","min":1,"max":"*","type":[{"code":"HumanName"}],"mustSupport":true,"isModifier":false,"mapping":[{"identity":"argonaut-dq-dstu2","map":"Patient.name"}]},{"id":"Patient.name.family","path":"Patient.name.family","min":1,"max":"1","type":[{"code":"string"}],"mustSupport":true,"isModifier":false,"mapping":[{"identity":"argonaut-dq-dstu2","map":"Patient.name.family"}]},{"id":"Patient.name.given","path":"Patient.name.given","min":1,"max":"*","type":[{"code":"string"}],"mustSupport":true,"isModifier":false,"mapping":[{"identity":"argonaut-dq-dstu2","map":"Patient.name.given"}]},{"id":"Patient.gender","path":"Patient.gender","min":1,"max":"1","type":[{"code":"code"}],"mustSupport":true,"isModifier":false,"binding":{"strength":"required","valueSet":"http://hl7.org/fhir/ValueSet/administrative-gender"},"mapping":[{"identity":"argonaut-dq-dstu2","map":"Patient.gender"}]},{"id":"Patient.birthDate","path":"Patient.birthDate","min":0,"max":"1","type":[{"code":"date"}],"mustSupport":true,"isModifier":false,"mapping":[{"identity":"argonaut-dq-dstu2","map":"Patient.birthDate"}]},{"id":"Patient.communication","path":"Patient.communication","min":0,"max":"*","mustSupport":true,"isModifier":false,"mapping":[{"identity":"argonaut-dq-dstu2","map":"Patient.communication"}]},{"id":"Patient.communication.language","path":"Patient.communication.language","min":1,"max":"1","type":[{"code":"CodeableConcept"}],"mustSupport":true,"isModifier":false,"binding":{"extension":[{"url":"http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet","valueCanonical":"http://hl7.org/fhir/us/core/ValueSet/simple-language"}],"strength":"extensible","valueSet":"http://hl7.org/fhir/us/core/ValueSet/simple-language"},"mapping":[{"identity":"argonaut-dq-dstu2","map":"Patient.communication.language"}]}]}} \ No newline at end of file +{ + "resourceType": "StructureDefinition", + "id": "us-core-patient", + "text": { + "status": "generated", + "div": "
\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n
NameFlagsCard.TypeDescription & Constraints\"doco\"
\".\"\".\" Patient
\".\"\".\"\".\" extension S0..1?? [CanonicalType[http://hl7.org/fhir/us/core/StructureDefinition/us-core-race]]URL: http://hl7.org/fhir/us/core/StructureDefinition/us-core-race
\".\"\".\"\".\" extension S0..1?? [CanonicalType[http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity]]URL: http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity
\".\"\".\"\".\" extension S0..1?? [CanonicalType[http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex]]URL: http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex
Binding: Birth Sex (required)
\".\"\".\"\".\" identifier S1..*Identifier
\".\"\".\"\".\"\".\" system S1..1uri
\".\"\".\"\".\"\".\" value S1..1stringThe value that is unique within the system.
\".\"\".\"\".\" name SI1..*HumanNameus-core-8: Patient.name.given or Patient.name.family or both SHALL be present
\".\"\".\"\".\"\".\" family SI0..1string
\".\"\".\"\".\"\".\" given SI0..*string
\".\"\".\"\".\" telecom S0..*
\".\"\".\"\".\"\".\" system S1..1Binding: ContactPointSystem (required)
\".\"\".\"\".\"\".\" value S1..1
\".\"\".\"\".\" gender S1..1codeBinding: AdministrativeGender (required)
\".\"\".\"\".\" birthDate S0..1date
\".\"\".\"\".\" address S0..*
\".\"\".\"\".\"\".\" line S0..*
\".\"\".\"\".\"\".\" city S0..1
\".\"\".\"\".\"\".\" state S0..1Binding: USPS Two Letter Alphabetic Codes (extensible)
\".\"\".\"\".\"\".\" postalCode S0..1US Zip Codes
\".\"\".\"\".\" communication S0..*
\".\"\".\"\".\"\".\" language S1..1CodeableConceptBinding: Language codes with language and optionally a region modifier (extensible)
Max Binding: Language codes with language and optionally a region modifier

\"doco\" Documentation for this format
" + }, + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient", + "version": "4.0.0", + "name": "USCorePatientProfile", + "title": "US Core Patient Profile", + "status": "active", + "experimental": false, + "date": "2019-05-21T00:00:00+00:00", + "publisher": "HL7 US Realm Steering Committee", + "contact": [ + { + "telecom": [ + { + "system": "url", + "value": "http://www.healthit.gov" + } + ] + } + ], + "description": "Defines constraints and extensions on the patient resource for the minimal set of data to query and retrieve patient demographic information.", + "jurisdiction": [ + { + "coding": [ + { + "system": "urn:iso:std:iso:3166", + "code": "US", + "display": "United States of America" + } + ] + } + ], + "fhirVersion": "4.0.0", + "mapping": [ + { + "identity": "argonaut-dq-dstu2", + "uri": "http://unknown.org/Argonaut-DQ-DSTU2", + "name": "Argonaut-DQ-DSTU2" + }, + { + "identity": "rim", + "uri": "http://hl7.org/v3", + "name": "RIM Mapping" + }, + { + "identity": "cda", + "uri": "http://hl7.org/v3/cda", + "name": "CDA (R2)" + }, + { + "identity": "w5", + "uri": "http://hl7.org/fhir/fivews", + "name": "FiveWs Pattern Mapping" + }, + { + "identity": "v2", + "uri": "http://hl7.org/v2", + "name": "HL7 v2 Mapping" + }, + { + "identity": "loinc", + "uri": "http://loinc.org", + "name": "LOINC code for the element" + } + ], + "kind": "resource", + "abstract": false, + "type": "Patient", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient", + "derivation": "constraint", + "snapshot": { + "element": [ + { + "id": "Patient", + "path": "Patient", + "short": "Information about an individual or animal receiving health care services", + "definition": "The US Core Patient Profile is based upon the core FHIR Patient Resource and designed to meet the applicable patient demographic data elements from the 2015 Edition Common Clinical Data Set.", + "alias": [ + "SubjectOfCare Client Resident" + ], + "min": 0, + "max": "*", + "base": { + "path": "Patient", + "min": 0, + "max": "*" + }, + "constraint": [ + { + "key": "dom-2", + "severity": "error", + "human": "If the resource is contained in another resource, it SHALL NOT contain nested Resources", + "expression": "contained.contained.empty()", + "xpath": "not(parent::f:contained and f:contained)", + "source": "DomainResource" + }, + { + "key": "dom-4", + "severity": "error", + "human": "If a resource is contained in another resource, it SHALL NOT have a meta.versionId or a meta.lastUpdated", + "expression": "contained.meta.versionId.empty() and contained.meta.lastUpdated.empty()", + "xpath": "not(exists(f:contained/*/f:meta/f:versionId)) and not(exists(f:contained/*/f:meta/f:lastUpdated))", + "source": "DomainResource" + }, + { + "key": "dom-3", + "severity": "error", + "human": "If the resource is contained in another resource, it SHALL be referred to from elsewhere in the resource or SHALL refer to the containing resource", + "expression": "contained.where((('#'+id in (%resource.descendants().reference | %resource.descendants().as(canonical) | %resource.descendants().as(uri) | %resource.descendants().as(url))) or descendants().where(reference = '#').exists() or descendants().where(as(canonical) = '#').exists() or descendants().where(as(canonical) = '#').exists()).not()).trace('unmatched', id).empty()", + "xpath": "not(exists(for $contained in f:contained return $contained[not(parent::*/descendant::f:reference/@value=concat('#', $contained/*/id/@value) or descendant::f:reference[@value='#'])]))", + "source": "DomainResource" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/elementdefinition-bestpractice", + "valueBoolean": true + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/elementdefinition-bestpractice-explanation", + "valueMarkdown": "When a resource has no narrative, only systems that fully understand the data can display the resource to a human safely. Including a human readable representation in the resource makes for a much more robust eco-system and cheaper handling of resources by intermediary systems. Some ecosystems restrict distribution of resources to only those systems that do fully understand the resources, and as a consequence implementers may believe that the narrative is superfluous. However experience shows that such eco-systems often open up to new participants over time." + } + ], + "key": "dom-6", + "severity": "warning", + "human": "A resource should have narrative for robust management", + "expression": "text.div.exists()", + "xpath": "exists(f:text/h:div)", + "source": "DomainResource" + }, + { + "key": "dom-5", + "severity": "error", + "human": "If a resource is contained in another resource, it SHALL NOT have a security label", + "expression": "contained.meta.security.empty()", + "xpath": "not(exists(f:contained/*/f:meta/f:security))", + "source": "DomainResource" + } + ], + "mustSupport": false, + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "rim", + "map": "Entity. Role, or Act" + }, + { + "identity": "rim", + "map": "Patient[classCode=PAT]" + }, + { + "identity": "cda", + "map": "ClinicalDocument.recordTarget.patientRole" + }, + { + "identity": "argonaut-dq-dstu2", + "map": "Patient" + } + ] + }, + { + "id": "Patient.id", + "path": "Patient.id", + "short": "Logical id of this artifact", + "definition": "The logical id of the resource, as used in the URL for the resource. Once assigned, this value never changes.", + "comment": "The only time that a resource does not have an id is when it is being submitted to the server using a create operation.", + "min": 0, + "max": "1", + "base": { + "path": "Resource.id", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "id" + } + ], + "isModifier": false, + "isSummary": true + }, + { + "id": "Patient.meta", + "path": "Patient.meta", + "short": "Metadata about the resource", + "definition": "The metadata about the resource. This is content that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource.", + "min": 0, + "max": "1", + "base": { + "path": "Resource.meta", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "Meta" + } + ], + "isModifier": false, + "isSummary": true + }, + { + "id": "Patient.implicitRules", + "path": "Patient.implicitRules", + "short": "A set of rules under which this content was created", + "definition": "A reference to a set of rules that were followed when the resource was constructed, and which must be understood when processing the content. Often, this is a reference to an implementation guide that defines the special rules along with other profiles etc.", + "comment": "Asserting this rule set restricts the content to be only understood by a limited set of trading partners. This inherently limits the usefulness of the data in the long term. However, the existing health eco-system is highly fractured, and not yet ready to define, collect, and exchange data in a generally computable sense. Wherever possible, implementers and/or specification writers should avoid using this element. Often, when used, the URL is a reference to an implementation guide that defines these special rules as part of it's narrative along with other profiles, value sets, etc.", + "min": 0, + "max": "1", + "base": { + "path": "Resource.implicitRules", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "uri" + } + ], + "isModifier": true, + "isModifierReason": "This element is labeled as a modifier because the implicit rules may provide additional knowledge about the resource that modifies it's meaning or interpretation", + "isSummary": true + }, + { + "id": "Patient.language", + "path": "Patient.language", + "short": "Language of the resource content", + "definition": "The base language in which the resource is written.", + "comment": "Language is provided to support indexing and accessibility (typically, services such as text to speech use the language tag). The html language tag in the narrative applies to the narrative. The language tag on the resource may be used to specify the language of other presentations generated from the data in the resource. Not all the content has to be in the base language. The Resource.language should not be assumed to apply to the narrative automatically. If a language is specified, it should it also be specified on the div element in the html (see rules in HTML5 for information about the relationship between xml:lang and the html lang attribute).", + "min": 0, + "max": "1", + "base": { + "path": "Resource.language", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "code" + } + ], + "isModifier": false, + "isSummary": false, + "binding": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet", + "valueCanonical": "http://hl7.org/fhir/ValueSet/all-languages" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString": "Language" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/elementdefinition-isCommonBinding", + "valueBoolean": true + } + ], + "strength": "preferred", + "description": "A human language.", + "valueSet": "http://hl7.org/fhir/ValueSet/languages" + } + }, + { + "id": "Patient.text", + "path": "Patient.text", + "short": "Text summary of the resource, for human interpretation", + "definition": "A human-readable narrative that contains a summary of the resource and can be used to represent the content of the resource to a human. The narrative need not encode all the structured data, but is required to contain sufficient detail to make it \"clinically safe\" for a human to just read the narrative. Resource definitions may define what content should be represented in the narrative to ensure clinical safety.", + "comment": "Contained resources do not have narrative. Resources that are not contained SHOULD have a narrative. In some cases, a resource may only have text with little or no additional discrete data (as long as all minOccurs=1 elements are satisfied). This may be necessary for data from legacy systems where information is captured as a \"text blob\" or where text is additionally entered raw or narrated and encoded information is added later.", + "alias": [ + "narrative", + "html", + "xhtml", + "display" + ], + "min": 0, + "max": "1", + "base": { + "path": "DomainResource.text", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "Narrative" + } + ], + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "rim", + "map": "Act.text?" + } + ] + }, + { + "id": "Patient.contained", + "path": "Patient.contained", + "short": "Contained, inline Resources", + "definition": "These resources do not have an independent existence apart from the resource that contains them - they cannot be identified independently, and nor can they have their own independent transaction scope.", + "comment": "This should never be done when the content can be identified properly, as once identification is lost, it is extremely difficult (and context dependent) to restore it again. Contained resources may have profiles and tags In their meta elements, but SHALL NOT have security labels.", + "alias": [ + "inline resources", + "anonymous resources", + "contained resources" + ], + "min": 0, + "max": "*", + "base": { + "path": "DomainResource.contained", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "Resource" + } + ], + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "rim", + "map": "N/A" + } + ] + }, + { + "id": "Patient.extension", + "path": "Patient.extension", + "slicing": { + "id": "1", + "discriminator": [ + { + "type": "value", + "path": "url" + } + ], + "ordered": false, + "rules": "open" + }, + "short": "Extension", + "definition": "An Extension", + "min": 0, + "max": "*", + "base": { + "path": "DomainResource.extension", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "Extension" + } + ], + "isModifier": false, + "isSummary": false + }, + { + "id": "Patient.extension:race", + "path": "Patient.extension", + "sliceName": "race", + "short": "Extension", + "definition": "An Extension", + "min": 0, + "max": "1", + "base": { + "path": "DomainResource.extension", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "Extension", + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race" + ] + } + ], + "mustSupport": true, + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.extension" + } + ] + }, + { + "id": "Patient.extension:ethnicity", + "path": "Patient.extension", + "sliceName": "ethnicity", + "short": "Extension", + "definition": "An Extension", + "min": 0, + "max": "1", + "base": { + "path": "DomainResource.extension", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "Extension", + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity" + ] + } + ], + "mustSupport": true, + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.extension" + } + ] + }, + { + "id": "Patient.extension:birthsex", + "path": "Patient.extension", + "sliceName": "birthsex", + "short": "Extension", + "definition": "An Extension", + "min": 0, + "max": "1", + "base": { + "path": "DomainResource.extension", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "Extension", + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex" + ] + } + ], + "mustSupport": true, + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.extension" + } + ] + }, + { + "id": "Patient.modifierExtension", + "path": "Patient.modifierExtension", + "short": "Extensions that cannot be ignored", + "definition": "May be used to represent additional information that is not part of the basic definition of the resource and that modifies the understanding of the element that contains it and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer is allowed to define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself).", + "comment": "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "requirements": "Modifier extensions allow for extensions that *cannot* be safely ignored to be clearly distinguished from the vast majority of extensions which can be safely ignored. This promotes interoperability by eliminating the need for implementers to prohibit the presence of extensions. For further information, see the [definition of modifier extensions](http://hl7.org/fhir/R4/extensibility.html#modifierExtension).", + "alias": [ + "extensions", + "user content" + ], + "min": 0, + "max": "*", + "base": { + "path": "DomainResource.modifierExtension", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "Extension" + } + ], + "isModifier": true, + "isModifierReason": "Modifier extensions are expected to modify the meaning or interpretation of the resource that contains them", + "isSummary": false, + "mapping": [ + { + "identity": "rim", + "map": "N/A" + } + ] + }, + { + "id": "Patient.identifier", + "path": "Patient.identifier", + "short": "An identifier for this patient", + "definition": "An identifier for this patient.", + "requirements": "Patients are almost always assigned specific numerical identifiers.", + "min": 1, + "max": "*", + "base": { + "path": "Patient.identifier", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "Identifier" + } + ], + "mustSupport": true, + "isModifier": false, + "isSummary": true, + "mapping": [ + { + "identity": "w5", + "map": "FiveWs.identifier" + }, + { + "identity": "v2", + "map": "PID-3" + }, + { + "identity": "rim", + "map": "id" + }, + { + "identity": "cda", + "map": ".id" + }, + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.identifier" + } + ] + }, + { + "id": "Patient.identifier.id", + "path": "Patient.identifier.id", + "representation": [ + "xmlAttr" + ], + "short": "Unique id for inter-element referencing", + "definition": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + "min": 0, + "max": "1", + "base": { + "path": "Element.id", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "string" + } + ], + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "rim", + "map": "n/a" + } + ] + }, + { + "id": "Patient.identifier.extension", + "path": "Patient.identifier.extension", + "slicing": { + "discriminator": [ + { + "type": "value", + "path": "url" + } + ], + "description": "Extensions are always sliced by (at least) url", + "rules": "open" + }, + "short": "Additional content defined by implementations", + "definition": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.", + "comment": "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "alias": [ + "extensions", + "user content" + ], + "min": 0, + "max": "*", + "base": { + "path": "Element.extension", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "Extension" + } + ], + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "rim", + "map": "n/a" + } + ] + }, + { + "id": "Patient.identifier.use", + "path": "Patient.identifier.use", + "short": "usual | official | temp | secondary | old (If known)", + "definition": "The purpose of this identifier.", + "comment": "Applications can assume that an identifier is permanent unless it explicitly says that it is temporary.", + "requirements": "Allows the appropriate identifier for a particular context of use to be selected from among a set of identifiers.", + "min": 0, + "max": "1", + "base": { + "path": "Identifier.use", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "code" + } + ], + "isModifier": true, + "isModifierReason": "This is labeled as \"Is Modifier\" because applications should not mistake a temporary id for a permanent one.", + "isSummary": true, + "binding": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString": "IdentifierUse" + } + ], + "strength": "required", + "description": "Identifies the purpose for this identifier, if known .", + "valueSet": "http://hl7.org/fhir/ValueSet/identifier-use|4.0.0" + }, + "mapping": [ + { + "identity": "v2", + "map": "N/A" + }, + { + "identity": "rim", + "map": "Role.code or implied by context" + } + ] + }, + { + "id": "Patient.identifier.type", + "path": "Patient.identifier.type", + "short": "Description of identifier", + "definition": "A coded type for the identifier that can be used to determine which identifier to use for a specific purpose.", + "comment": "This element deals only with general categories of identifiers. It SHOULD not be used for codes that correspond 1..1 with the Identifier.system. Some identifiers may fall into multiple categories due to common usage. Where the system is known, a type is unnecessary because the type is always part of the system definition. However systems often need to handle identifiers where the system is not known. There is not a 1:1 relationship between type and system, since many different systems have the same type.", + "requirements": "Allows users to make use of identifiers when the identifier system is not known.", + "min": 0, + "max": "1", + "base": { + "path": "Identifier.type", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "CodeableConcept" + } + ], + "isModifier": false, + "isSummary": true, + "binding": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString": "IdentifierType" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/elementdefinition-isCommonBinding", + "valueBoolean": true + } + ], + "strength": "extensible", + "description": "A coded type for an identifier that can be used to determine which identifier to use for a specific purpose.", + "valueSet": "http://hl7.org/fhir/ValueSet/identifier-type" + }, + "mapping": [ + { + "identity": "v2", + "map": "CX.5" + }, + { + "identity": "rim", + "map": "Role.code or implied by context" + } + ] + }, + { + "id": "Patient.identifier.system", + "path": "Patient.identifier.system", + "short": "The namespace for the identifier value", + "definition": "Establishes the namespace for the value - that is, a URL that describes a set values that are unique.", + "comment": "Identifier.system is always case sensitive.", + "requirements": "There are many sets of identifiers. To perform matching of two identifiers, we need to know what set we're dealing with. The system identifies a particular set of unique identifiers.", + "min": 1, + "max": "1", + "base": { + "path": "Identifier.system", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "uri" + } + ], + "example": [ + { + "label": "General", + "valueUri": "http://www.acme.com/identifiers/patient" + } + ], + "mustSupport": true, + "isModifier": false, + "isSummary": true, + "mapping": [ + { + "identity": "v2", + "map": "CX.4 / EI-2-4" + }, + { + "identity": "rim", + "map": "II.root or Role.id.root" + }, + { + "identity": "servd", + "map": "./IdentifierType" + }, + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.identifier.system" + } + ] + }, + { + "id": "Patient.identifier.value", + "path": "Patient.identifier.value", + "short": "The value that is unique within the system.", + "definition": "The portion of the identifier typically relevant to the user and which is unique within the context of the system.", + "comment": "If the value is a full URI, then the system SHALL be urn:ietf:rfc:3986. The value's primary purpose is computational mapping. As a result, it may be normalized for comparison purposes (e.g. removing non-significant whitespace, dashes, etc.) A value formatted for human display can be conveyed using the [Rendered Value extension](http://hl7.org/fhir/R4/extension-rendered-value.html). Identifier.value is to be treated as case sensitive unless knowledge of the Identifier.system allows the processer to be confident that non-case-sensitive processing is safe.", + "min": 1, + "max": "1", + "base": { + "path": "Identifier.value", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "string" + } + ], + "example": [ + { + "label": "General", + "valueString": "123456" + } + ], + "mustSupport": true, + "isModifier": false, + "isSummary": true, + "mapping": [ + { + "identity": "v2", + "map": "CX.1 / EI.1" + }, + { + "identity": "rim", + "map": "II.extension or II.root if system indicates OID or GUID (Or Role.id.extension or root)" + }, + { + "identity": "servd", + "map": "./Value" + }, + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.identifier.value" + } + ] + }, + { + "id": "Patient.identifier.period", + "path": "Patient.identifier.period", + "short": "Time period when id is/was valid for use", + "definition": "Time period during which identifier is/was valid for use.", + "min": 0, + "max": "1", + "base": { + "path": "Identifier.period", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "Period" + } + ], + "isModifier": false, + "isSummary": true, + "mapping": [ + { + "identity": "v2", + "map": "CX.7 + CX.8" + }, + { + "identity": "rim", + "map": "Role.effectiveTime or implied by context" + }, + { + "identity": "servd", + "map": "./StartDate and ./EndDate" + } + ] + }, + { + "id": "Patient.identifier.assigner", + "path": "Patient.identifier.assigner", + "short": "Organization that issued id (may be just text)", + "definition": "Organization that issued/manages the identifier.", + "comment": "The Identifier.assigner may omit the .reference element and only contain a .display element reflecting the name or other textual information about the assigning organization.", + "min": 0, + "max": "1", + "base": { + "path": "Identifier.assigner", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "Reference", + "targetProfile": [ + "http://hl7.org/fhir/StructureDefinition/Organization" + ] + } + ], + "isModifier": false, + "isSummary": true, + "mapping": [ + { + "identity": "v2", + "map": "CX.4 / (CX.4,CX.9,CX.10)" + }, + { + "identity": "rim", + "map": "II.assigningAuthorityName but note that this is an improper use by the definition of the field. Also Role.scoper" + }, + { + "identity": "servd", + "map": "./IdentifierIssuingAuthority" + } + ] + }, + { + "id": "Patient.active", + "path": "Patient.active", + "short": "Whether this patient's record is in active use", + "definition": "Whether this patient record is in active use. \nMany systems use this property to mark as non-current patients, such as those that have not been seen for a period of time based on an organization's business rules.\n\nIt is often used to filter patient lists to exclude inactive patients\n\nDeceased patients may also be marked as inactive for the same reasons, but may be active for some time after death.", + "comment": "If a record is inactive, and linked to an active record, then future patient/record updates should occur on the other patient.", + "requirements": "Need to be able to mark a patient record as not to be used because it was created in error.", + "min": 0, + "max": "1", + "base": { + "path": "Patient.active", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "boolean" + } + ], + "meaningWhenMissing": "This resource is generally assumed to be active if no value is provided for the active element", + "isModifier": true, + "isModifierReason": "This element is labelled as a modifier because it is a status element that can indicate that a record should not be treated as valid", + "isSummary": true, + "mapping": [ + { + "identity": "w5", + "map": "FiveWs.status" + }, + { + "identity": "rim", + "map": "statusCode" + }, + { + "identity": "cda", + "map": "n/a" + } + ] + }, + { + "id": "Patient.name", + "path": "Patient.name", + "short": "A name associated with the patient", + "definition": "A name associated with the individual.", + "comment": "A patient may have multiple names with different uses or applicable periods. For animals, the name is a \"HumanName\" in the sense that is assigned and used by humans and has the same patterns.", + "requirements": "Need to be able to track the patient by multiple names. Examples are your official name and a partner name.", + "min": 1, + "max": "*", + "base": { + "path": "Patient.name", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "HumanName" + } + ], + "constraint": [ + { + "key": "us-core-8", + "severity": "error", + "human": "Patient.name.given or Patient.name.family or both SHALL be present", + "expression": "family.exists() or given.exists()", + "xpath": "f:given or f:family" + } + ], + "mustSupport": true, + "isModifier": false, + "isSummary": true, + "mapping": [ + { + "identity": "v2", + "map": "PID-5, PID-9" + }, + { + "identity": "rim", + "map": "name" + }, + { + "identity": "cda", + "map": ".patient.name" + }, + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.name" + } + ] + }, + { + "id": "Patient.name.id", + "path": "Patient.name.id", + "representation": [ + "xmlAttr" + ], + "short": "Unique id for inter-element referencing", + "definition": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + "min": 0, + "max": "1", + "base": { + "path": "Element.id", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "string" + } + ], + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "rim", + "map": "n/a" + } + ] + }, + { + "id": "Patient.name.extension", + "path": "Patient.name.extension", + "slicing": { + "discriminator": [ + { + "type": "value", + "path": "url" + } + ], + "description": "Extensions are always sliced by (at least) url", + "rules": "open" + }, + "short": "Additional content defined by implementations", + "definition": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.", + "comment": "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "alias": [ + "extensions", + "user content" + ], + "min": 0, + "max": "*", + "base": { + "path": "Element.extension", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "Extension" + } + ], + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "rim", + "map": "n/a" + } + ] + }, + { + "id": "Patient.name.use", + "path": "Patient.name.use", + "short": "usual | official | temp | nickname | anonymous | old | maiden", + "definition": "Identifies the purpose for this name.", + "comment": "Applications can assume that a name is current unless it explicitly says that it is temporary or old.", + "requirements": "Allows the appropriate name for a particular context of use to be selected from among a set of names.", + "min": 0, + "max": "1", + "base": { + "path": "HumanName.use", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "code" + } + ], + "isModifier": true, + "isModifierReason": "This is labeled as \"Is Modifier\" because applications should not mistake a temporary or old name etc.for a current/permanent one", + "isSummary": true, + "binding": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString": "NameUse" + } + ], + "strength": "required", + "description": "The use of a human name.", + "valueSet": "http://hl7.org/fhir/ValueSet/name-use|4.0.0" + }, + "mapping": [ + { + "identity": "v2", + "map": "XPN.7, but often indicated by which field contains the name" + }, + { + "identity": "rim", + "map": "unique(./use)" + }, + { + "identity": "servd", + "map": "./NamePurpose" + } + ] + }, + { + "id": "Patient.name.text", + "path": "Patient.name.text", + "short": "Text representation of the full name", + "definition": "Specifies the entire name as it should be displayed e.g. on an application UI. This may be provided instead of or as well as the specific parts.", + "comment": "Can provide both a text representation and parts. Applications updating a name SHALL ensure that when both text and parts are present, no content is included in the text that isn't found in a part.", + "requirements": "A renderable, unencoded form.", + "min": 0, + "max": "1", + "base": { + "path": "HumanName.text", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "string" + } + ], + "isModifier": false, + "isSummary": true, + "mapping": [ + { + "identity": "v2", + "map": "implied by XPN.11" + }, + { + "identity": "rim", + "map": "./formatted" + } + ] + }, + { + "id": "Patient.name.family", + "path": "Patient.name.family", + "short": "Family name (often called 'Surname')", + "definition": "The part of a name that links to the genealogy. In some cultures (e.g. Eritrea) the family name of a son is the first name of his father.", + "comment": "Family Name may be decomposed into specific parts using extensions (de, nl, es related cultures).", + "alias": [ + "surname" + ], + "min": 0, + "max": "1", + "base": { + "path": "HumanName.family", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "string" + } + ], + "mustSupport": true, + "isModifier": false, + "isSummary": true, + "mapping": [ + { + "identity": "v2", + "map": "XPN.1/FN.1" + }, + { + "identity": "rim", + "map": "./part[partType = FAM]" + }, + { + "identity": "servd", + "map": "./FamilyName" + }, + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.name.family" + } + ] + }, + { + "id": "Patient.name.given", + "path": "Patient.name.given", + "short": "Given names (not always 'first'). Includes middle names", + "definition": "Given name.", + "comment": "If only initials are recorded, they may be used in place of the full name parts. Initials may be separated into multiple given names but often aren't due to paractical limitations. This element is not called \"first name\" since given names do not always come first.", + "alias": [ + "first name", + "middle name" + ], + "min": 0, + "max": "*", + "base": { + "path": "HumanName.given", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "string" + } + ], + "orderMeaning": "Given Names appear in the correct order for presenting the name", + "mustSupport": true, + "isModifier": false, + "isSummary": true, + "mapping": [ + { + "identity": "v2", + "map": "XPN.2 + XPN.3" + }, + { + "identity": "rim", + "map": "./part[partType = GIV]" + }, + { + "identity": "servd", + "map": "./GivenNames" + }, + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.name.given" + } + ] + }, + { + "id": "Patient.name.prefix", + "path": "Patient.name.prefix", + "short": "Parts that come before the name", + "definition": "Part of the name that is acquired as a title due to academic, legal, employment or nobility status, etc. and that appears at the start of the name.", + "min": 0, + "max": "*", + "base": { + "path": "HumanName.prefix", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "string" + } + ], + "orderMeaning": "Prefixes appear in the correct order for presenting the name", + "isModifier": false, + "isSummary": true, + "mapping": [ + { + "identity": "v2", + "map": "XPN.5" + }, + { + "identity": "rim", + "map": "./part[partType = PFX]" + }, + { + "identity": "servd", + "map": "./TitleCode" + } + ] + }, + { + "id": "Patient.name.suffix", + "path": "Patient.name.suffix", + "short": "Parts that come after the name", + "definition": "Part of the name that is acquired as a title due to academic, legal, employment or nobility status, etc. and that appears at the end of the name.", + "min": 0, + "max": "*", + "base": { + "path": "HumanName.suffix", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "string" + } + ], + "orderMeaning": "Suffixes appear in the correct order for presenting the name", + "isModifier": false, + "isSummary": true, + "mapping": [ + { + "identity": "v2", + "map": "XPN/4" + }, + { + "identity": "rim", + "map": "./part[partType = SFX]" + } + ] + }, + { + "id": "Patient.name.period", + "path": "Patient.name.period", + "short": "Time period when name was/is in use", + "definition": "Indicates the period of time when this name was valid for the named person.", + "requirements": "Allows names to be placed in historical context.", + "min": 0, + "max": "1", + "base": { + "path": "HumanName.period", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "Period" + } + ], + "isModifier": false, + "isSummary": true, + "mapping": [ + { + "identity": "v2", + "map": "XPN.13 + XPN.14" + }, + { + "identity": "rim", + "map": "./usablePeriod[type=\"IVL\"]" + }, + { + "identity": "servd", + "map": "./StartDate and ./EndDate" + } + ] + }, + { + "id": "Patient.telecom", + "path": "Patient.telecom", + "short": "A contact detail for the individual", + "definition": "A contact detail (e.g. a telephone number or an email address) by which the individual may be contacted.", + "comment": "A Patient may have multiple ways to be contacted with different uses or applicable periods. May need to have options for contacting the person urgently and also to help with identification. The address might not go directly to the individual, but may reach another party that is able to proxy for the patient (i.e. home phone, or pet owner's phone).", + "requirements": "People have (primary) ways to contact them in some way such as phone, email.", + "min": 0, + "max": "*", + "base": { + "path": "Patient.telecom", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "ContactPoint" + } + ], + "mustSupport": true, + "isModifier": false, + "isSummary": true, + "mapping": [ + { + "identity": "v2", + "map": "PID-13, PID-14, PID-40" + }, + { + "identity": "rim", + "map": "telecom" + }, + { + "identity": "cda", + "map": ".telecom" + }, + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.gender" + } + ] + }, + { + "id": "Patient.telecom.id", + "path": "Patient.telecom.id", + "representation": [ + "xmlAttr" + ], + "short": "Unique id for inter-element referencing", + "definition": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + "min": 0, + "max": "1", + "base": { + "path": "Element.id", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "string" + } + ], + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "rim", + "map": "n/a" + } + ] + }, + { + "id": "Patient.telecom.extension", + "path": "Patient.telecom.extension", + "slicing": { + "discriminator": [ + { + "type": "value", + "path": "url" + } + ], + "description": "Extensions are always sliced by (at least) url", + "rules": "open" + }, + "short": "Additional content defined by implementations", + "definition": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.", + "comment": "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "alias": [ + "extensions", + "user content" + ], + "min": 0, + "max": "*", + "base": { + "path": "Element.extension", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "Extension" + } + ], + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "rim", + "map": "n/a" + } + ] + }, + { + "id": "Patient.telecom.system", + "path": "Patient.telecom.system", + "short": "phone | fax | email | pager | url | sms | other", + "definition": "Telecommunications form for contact point - what communications system is required to make use of the contact.", + "min": 1, + "max": "1", + "base": { + "path": "ContactPoint.system", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "code" + } + ], + "condition": [ + "cpt-2" + ], + "mustSupport": true, + "isModifier": false, + "isSummary": true, + "binding": { + "strength": "required", + "description": "Telecommunications form for contact point.", + "valueSet": "http://hl7.org/fhir/ValueSet/contact-point-system" + }, + "mapping": [ + { + "identity": "v2", + "map": "XTN.3" + }, + { + "identity": "rim", + "map": "./scheme" + }, + { + "identity": "servd", + "map": "./ContactPointType" + }, + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.gender" + } + ] + }, + { + "id": "Patient.telecom.value", + "path": "Patient.telecom.value", + "short": "The actual contact point details", + "definition": "The actual contact point details, in a form that is meaningful to the designated communication system (i.e. phone number or email address).", + "comment": "Additional text data such as phone extension numbers, or notes about use of the contact are sometimes included in the value.", + "requirements": "Need to support legacy numbers that are not in a tightly controlled format.", + "min": 1, + "max": "1", + "base": { + "path": "ContactPoint.value", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "string" + } + ], + "mustSupport": true, + "isModifier": false, + "isSummary": true, + "mapping": [ + { + "identity": "v2", + "map": "XTN.1 (or XTN.12)" + }, + { + "identity": "rim", + "map": "./url" + }, + { + "identity": "servd", + "map": "./Value" + }, + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.gender" + } + ] + }, + { + "id": "Patient.telecom.use", + "path": "Patient.telecom.use", + "short": "home | work | temp | old | mobile - purpose of this contact point", + "definition": "Identifies the purpose for the contact point.", + "comment": "Applications can assume that a contact is current unless it explicitly says that it is temporary or old.", + "requirements": "Need to track the way a person uses this contact, so a user can choose which is appropriate for their purpose.", + "min": 0, + "max": "1", + "base": { + "path": "ContactPoint.use", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "code" + } + ], + "isModifier": true, + "isModifierReason": "This is labeled as \"Is Modifier\" because applications should not mistake a temporary or old contact etc.for a current/permanent one", + "isSummary": true, + "binding": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString": "ContactPointUse" + } + ], + "strength": "required", + "description": "Use of contact point.", + "valueSet": "http://hl7.org/fhir/ValueSet/contact-point-use|4.0.0" + }, + "mapping": [ + { + "identity": "v2", + "map": "XTN.2 - but often indicated by field" + }, + { + "identity": "rim", + "map": "unique(./use)" + }, + { + "identity": "servd", + "map": "./ContactPointPurpose" + } + ] + }, + { + "id": "Patient.telecom.rank", + "path": "Patient.telecom.rank", + "short": "Specify preferred order of use (1 = highest)", + "definition": "Specifies a preferred order in which to use a set of contacts. ContactPoints with lower rank values are more preferred than those with higher rank values.", + "comment": "Note that rank does not necessarily follow the order in which the contacts are represented in the instance.", + "min": 0, + "max": "1", + "base": { + "path": "ContactPoint.rank", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "positiveInt" + } + ], + "isModifier": false, + "isSummary": true, + "mapping": [ + { + "identity": "v2", + "map": "n/a" + }, + { + "identity": "rim", + "map": "n/a" + } + ] + }, + { + "id": "Patient.telecom.period", + "path": "Patient.telecom.period", + "short": "Time period when the contact point was/is in use", + "definition": "Time period when the contact point was/is in use.", + "min": 0, + "max": "1", + "base": { + "path": "ContactPoint.period", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "Period" + } + ], + "isModifier": false, + "isSummary": true, + "mapping": [ + { + "identity": "v2", + "map": "N/A" + }, + { + "identity": "rim", + "map": "./usablePeriod[type=\"IVL\"]" + }, + { + "identity": "servd", + "map": "./StartDate and ./EndDate" + } + ] + }, + { + "id": "Patient.gender", + "path": "Patient.gender", + "short": "male | female | other | unknown", + "definition": "Administrative Gender - the gender that the patient is considered to have for administration and record keeping purposes.", + "comment": "The gender might not match the biological sex as determined by genetics or the individual's preferred identification. Note that for both humans and particularly animals, there are other legitimate possibilities than male and female, though the vast majority of systems and contexts only support male and female. Systems providing decision support or enforcing business rules should ideally do this on the basis of Observations dealing with the specific sex or gender aspect of interest (anatomical, chromosomal, social, etc.) However, because these observations are infrequently recorded, defaulting to the administrative gender is common practice. Where such defaulting occurs, rule enforcement should allow for the variation between administrative and biological, chromosomal and other gender aspects. For example, an alert about a hysterectomy on a male should be handled as a warning or overridable error, not a \"hard\" error. See the Patient Gender and Sex section for additional information about communicating patient gender and sex.", + "requirements": "Needed for identification of the individual, in combination with (at least) name and birth date.", + "min": 1, + "max": "1", + "base": { + "path": "Patient.gender", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "code" + } + ], + "mustSupport": true, + "isModifier": false, + "isSummary": true, + "binding": { + "strength": "required", + "valueSet": "http://hl7.org/fhir/ValueSet/administrative-gender" + }, + "mapping": [ + { + "identity": "v2", + "map": "PID-8" + }, + { + "identity": "rim", + "map": "player[classCode=PSN|ANM and determinerCode=INSTANCE]/administrativeGender" + }, + { + "identity": "cda", + "map": ".patient.administrativeGenderCode" + }, + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.gender" + } + ] + }, + { + "id": "Patient.birthDate", + "path": "Patient.birthDate", + "short": "The date of birth for the individual", + "definition": "The date of birth for the individual.", + "comment": "At least an estimated year should be provided as a guess if the real DOB is unknown There is a standard extension \"patient-birthTime\" available that should be used where Time is required (such as in maternity/infant care systems).", + "requirements": "Age of the individual drives many clinical processes.", + "min": 0, + "max": "1", + "base": { + "path": "Patient.birthDate", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "date" + } + ], + "mustSupport": true, + "isModifier": false, + "isSummary": true, + "mapping": [ + { + "identity": "v2", + "map": "PID-7" + }, + { + "identity": "rim", + "map": "player[classCode=PSN|ANM and determinerCode=INSTANCE]/birthTime" + }, + { + "identity": "cda", + "map": ".patient.birthTime" + }, + { + "identity": "loinc", + "map": "21112-8" + }, + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.birthDate" + } + ] + }, + { + "id": "Patient.deceased[x]", + "path": "Patient.deceased[x]", + "short": "Indicates if the individual is deceased or not", + "definition": "Indicates if the individual is deceased or not.", + "comment": "If there's no value in the instance, it means there is no statement on whether or not the individual is deceased. Most systems will interpret the absence of a value as a sign of the person being alive.", + "requirements": "The fact that a patient is deceased influences the clinical process. Also, in human communication and relation management it is necessary to know whether the person is alive.", + "min": 0, + "max": "1", + "base": { + "path": "Patient.deceased[x]", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "boolean" + }, + { + "code": "dateTime" + } + ], + "isModifier": true, + "isModifierReason": "This element is labeled as a modifier because once a patient is marked as deceased, the actions that are appropriate to perform on the patient may be significantly different.", + "isSummary": true, + "mapping": [ + { + "identity": "v2", + "map": "PID-30 (bool) and PID-29 (datetime)" + }, + { + "identity": "rim", + "map": "player[classCode=PSN|ANM and determinerCode=INSTANCE]/deceasedInd, player[classCode=PSN|ANM and determinerCode=INSTANCE]/deceasedTime" + }, + { + "identity": "cda", + "map": "n/a" + } + ] + }, + { + "id": "Patient.address", + "path": "Patient.address", + "short": "An address for the individual", + "definition": "An address for the individual.", + "comment": "Patient may have multiple addresses with different uses or applicable periods.", + "requirements": "May need to keep track of patient addresses for contacting, billing or reporting requirements and also to help with identification.", + "min": 0, + "max": "*", + "base": { + "path": "Patient.address", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "Address" + } + ], + "mustSupport": true, + "isModifier": false, + "isSummary": true, + "mapping": [ + { + "identity": "v2", + "map": "PID-11" + }, + { + "identity": "rim", + "map": "addr" + }, + { + "identity": "cda", + "map": ".addr" + }, + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.birthDate" + } + ] + }, + { + "id": "Patient.address.id", + "path": "Patient.address.id", + "representation": [ + "xmlAttr" + ], + "short": "Unique id for inter-element referencing", + "definition": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + "min": 0, + "max": "1", + "base": { + "path": "Element.id", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "string" + } + ], + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "rim", + "map": "n/a" + } + ] + }, + { + "id": "Patient.address.extension", + "path": "Patient.address.extension", + "slicing": { + "discriminator": [ + { + "type": "value", + "path": "url" + } + ], + "description": "Extensions are always sliced by (at least) url", + "rules": "open" + }, + "short": "Additional content defined by implementations", + "definition": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.", + "comment": "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "alias": [ + "extensions", + "user content" + ], + "min": 0, + "max": "*", + "base": { + "path": "Element.extension", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "Extension" + } + ], + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "rim", + "map": "n/a" + } + ] + }, + { + "id": "Patient.address.use", + "path": "Patient.address.use", + "short": "home | work | temp | old | billing - purpose of this address", + "definition": "The purpose of this address.", + "comment": "Applications can assume that an address is current unless it explicitly says that it is temporary or old.", + "requirements": "Allows an appropriate address to be chosen from a list of many.", + "min": 0, + "max": "1", + "base": { + "path": "Address.use", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "code" + } + ], + "example": [ + { + "label": "General", + "valueCode": "home" + } + ], + "isModifier": true, + "isModifierReason": "This is labeled as \"Is Modifier\" because applications should not mistake a temporary or old address etc.for a current/permanent one", + "isSummary": true, + "binding": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString": "AddressUse" + } + ], + "strength": "required", + "description": "The use of an address.", + "valueSet": "http://hl7.org/fhir/ValueSet/address-use|4.0.0" + }, + "mapping": [ + { + "identity": "v2", + "map": "XAD.7" + }, + { + "identity": "rim", + "map": "unique(./use)" + }, + { + "identity": "servd", + "map": "./AddressPurpose" + } + ] + }, + { + "id": "Patient.address.type", + "path": "Patient.address.type", + "short": "postal | physical | both", + "definition": "Distinguishes between physical addresses (those you can visit) and mailing addresses (e.g. PO Boxes and care-of addresses). Most addresses are both.", + "comment": "The definition of Address states that \"address is intended to describe postal addresses, not physical locations\". However, many applications track whether an address has a dual purpose of being a location that can be visited as well as being a valid delivery destination, and Postal addresses are often used as proxies for physical locations (also see the [Location](http://hl7.org/fhir/R4/location.html#) resource).", + "min": 0, + "max": "1", + "base": { + "path": "Address.type", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "code" + } + ], + "example": [ + { + "label": "General", + "valueCode": "both" + } + ], + "isModifier": false, + "isSummary": true, + "binding": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString": "AddressType" + } + ], + "strength": "required", + "description": "The type of an address (physical / postal).", + "valueSet": "http://hl7.org/fhir/ValueSet/address-type|4.0.0" + }, + "mapping": [ + { + "identity": "v2", + "map": "XAD.18" + }, + { + "identity": "rim", + "map": "unique(./use)" + }, + { + "identity": "vcard", + "map": "address type parameter" + } + ] + }, + { + "id": "Patient.address.text", + "path": "Patient.address.text", + "short": "Text representation of the address", + "definition": "Specifies the entire address as it should be displayed e.g. on a postal label. This may be provided instead of or as well as the specific parts.", + "comment": "Can provide both a text representation and parts. Applications updating an address SHALL ensure that when both text and parts are present, no content is included in the text that isn't found in a part.", + "requirements": "A renderable, unencoded form.", + "min": 0, + "max": "1", + "base": { + "path": "Address.text", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "string" + } + ], + "example": [ + { + "label": "General", + "valueString": "137 Nowhere Street, Erewhon 9132" + } + ], + "isModifier": false, + "isSummary": true, + "mapping": [ + { + "identity": "v2", + "map": "XAD.1 + XAD.2 + XAD.3 + XAD.4 + XAD.5 + XAD.6" + }, + { + "identity": "rim", + "map": "./formatted" + }, + { + "identity": "vcard", + "map": "address label parameter" + } + ] + }, + { + "id": "Patient.address.line", + "path": "Patient.address.line", + "short": "Street name, number, direction & P.O. Box etc.", + "definition": "This component contains the house number, apartment number, street name, street direction, P.O. Box number, delivery hints, and similar address information.", + "min": 0, + "max": "*", + "base": { + "path": "Address.line", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "string" + } + ], + "orderMeaning": "The order in which lines should appear in an address label", + "example": [ + { + "label": "General", + "valueString": "137 Nowhere Street" + } + ], + "mustSupport": true, + "isModifier": false, + "isSummary": true, + "mapping": [ + { + "identity": "v2", + "map": "XAD.1 + XAD.2 (note: XAD.1 and XAD.2 have different meanings for a company address than for a person address)" + }, + { + "identity": "rim", + "map": "AD.part[parttype = AL]" + }, + { + "identity": "vcard", + "map": "street" + }, + { + "identity": "servd", + "map": "./StreetAddress (newline delimitted)" + }, + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.birthDate" + } + ] + }, + { + "id": "Patient.address.city", + "path": "Patient.address.city", + "short": "Name of city, town etc.", + "definition": "The name of the city, town, suburb, village or other community or delivery center.", + "alias": [ + "Municpality" + ], + "min": 0, + "max": "1", + "base": { + "path": "Address.city", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "string" + } + ], + "example": [ + { + "label": "General", + "valueString": "Erewhon" + } + ], + "mustSupport": true, + "isModifier": false, + "isSummary": true, + "mapping": [ + { + "identity": "v2", + "map": "XAD.3" + }, + { + "identity": "rim", + "map": "AD.part[parttype = CTY]" + }, + { + "identity": "vcard", + "map": "locality" + }, + { + "identity": "servd", + "map": "./Jurisdiction" + }, + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.birthDate" + } + ] + }, + { + "id": "Patient.address.district", + "path": "Patient.address.district", + "short": "District name (aka county)", + "definition": "The name of the administrative area (county).", + "comment": "District is sometimes known as county, but in some regions 'county' is used in place of city (municipality), so county name should be conveyed in city instead.", + "alias": [ + "County" + ], + "min": 0, + "max": "1", + "base": { + "path": "Address.district", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "string" + } + ], + "example": [ + { + "label": "General", + "valueString": "Madison" + } + ], + "isModifier": false, + "isSummary": true, + "mapping": [ + { + "identity": "v2", + "map": "XAD.9" + }, + { + "identity": "rim", + "map": "AD.part[parttype = CNT | CPA]" + } + ] + }, + { + "id": "Patient.address.state", + "path": "Patient.address.state", + "short": "Sub-unit of country (abbreviations ok)", + "definition": "Sub-unit of a country with limited sovereignty in a federally organized country. A code may be used if codes are in common use (e.g. US 2 letter state codes).", + "alias": [ + "Province", + "Territory" + ], + "min": 0, + "max": "1", + "base": { + "path": "Address.state", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "string" + } + ], + "mustSupport": true, + "isModifier": false, + "isSummary": true, + "binding": { + "strength": "extensible", + "description": "Two Letter USPS alphabetic codes.", + "valueSet": "http://hl7.org/fhir/us/core/ValueSet/us-core-usps-state" + }, + "mapping": [ + { + "identity": "v2", + "map": "XAD.4" + }, + { + "identity": "rim", + "map": "AD.part[parttype = STA]" + }, + { + "identity": "vcard", + "map": "region" + }, + { + "identity": "servd", + "map": "./Region" + }, + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.birthDate" + } + ] + }, + { + "id": "Patient.address.postalCode", + "path": "Patient.address.postalCode", + "short": "US Zip Codes", + "definition": "A postal code designating a region defined by the postal service.", + "alias": [ + "Zip", + "Zip Code" + ], + "min": 0, + "max": "1", + "base": { + "path": "Address.postalCode", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "string" + } + ], + "example": [ + { + "label": "General", + "valueString": "9132" + } + ], + "mustSupport": true, + "isModifier": false, + "isSummary": true, + "mapping": [ + { + "identity": "v2", + "map": "XAD.5" + }, + { + "identity": "rim", + "map": "AD.part[parttype = ZIP]" + }, + { + "identity": "vcard", + "map": "code" + }, + { + "identity": "servd", + "map": "./PostalIdentificationCode" + }, + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.birthDate" + } + ] + }, + { + "id": "Patient.address.country", + "path": "Patient.address.country", + "short": "Country (e.g. can be ISO 3166 2 or 3 letter code)", + "definition": "Country - a nation as commonly understood or generally accepted.", + "comment": "ISO 3166 3 letter codes can be used in place of a human readable country name.", + "min": 0, + "max": "1", + "base": { + "path": "Address.country", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "string" + } + ], + "isModifier": false, + "isSummary": true, + "mapping": [ + { + "identity": "v2", + "map": "XAD.6" + }, + { + "identity": "rim", + "map": "AD.part[parttype = CNT]" + }, + { + "identity": "vcard", + "map": "country" + }, + { + "identity": "servd", + "map": "./Country" + } + ] + }, + { + "id": "Patient.address.period", + "path": "Patient.address.period", + "short": "Time period when address was/is in use", + "definition": "Time period when address was/is in use.", + "requirements": "Allows addresses to be placed in historical context.", + "min": 0, + "max": "1", + "base": { + "path": "Address.period", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "Period" + } + ], + "example": [ + { + "label": "General", + "valuePeriod": { + "start": "2010-03-23T00:00:00+00:00", + "end": "2010-07-01T00:00:00+00:00" + } + } + ], + "isModifier": false, + "isSummary": true, + "mapping": [ + { + "identity": "v2", + "map": "XAD.12 / XAD.13 + XAD.14" + }, + { + "identity": "rim", + "map": "./usablePeriod[type=\"IVL\"]" + }, + { + "identity": "servd", + "map": "./StartDate and ./EndDate" + } + ] + }, + { + "id": "Patient.maritalStatus", + "path": "Patient.maritalStatus", + "short": "Marital (civil) status of a patient", + "definition": "This field contains a patient's most recent marital (civil) status.", + "requirements": "Most, if not all systems capture it.", + "min": 0, + "max": "1", + "base": { + "path": "Patient.maritalStatus", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "CodeableConcept" + } + ], + "isModifier": false, + "isSummary": false, + "binding": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString": "MaritalStatus" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/elementdefinition-isCommonBinding", + "valueBoolean": true + } + ], + "strength": "extensible", + "description": "The domestic partnership status of a person.", + "valueSet": "http://hl7.org/fhir/ValueSet/marital-status" + }, + "mapping": [ + { + "identity": "v2", + "map": "PID-16" + }, + { + "identity": "rim", + "map": "player[classCode=PSN]/maritalStatusCode" + }, + { + "identity": "cda", + "map": ".patient.maritalStatusCode" + } + ] + }, + { + "id": "Patient.multipleBirth[x]", + "path": "Patient.multipleBirth[x]", + "short": "Whether patient is part of a multiple birth", + "definition": "Indicates whether the patient is part of a multiple (boolean) or indicates the actual birth order (integer).", + "comment": "Where the valueInteger is provided, the number is the birth number in the sequence. E.g. The middle birth in triplets would be valueInteger=2 and the third born would have valueInteger=3 If a boolean value was provided for this triplets example, then all 3 patient records would have valueBoolean=true (the ordering is not indicated).", + "requirements": "For disambiguation of multiple-birth children, especially relevant where the care provider doesn't meet the patient, such as labs.", + "min": 0, + "max": "1", + "base": { + "path": "Patient.multipleBirth[x]", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "boolean" + }, + { + "code": "integer" + } + ], + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "v2", + "map": "PID-24 (bool), PID-25 (integer)" + }, + { + "identity": "rim", + "map": "player[classCode=PSN|ANM and determinerCode=INSTANCE]/multipleBirthInd, player[classCode=PSN|ANM and determinerCode=INSTANCE]/multipleBirthOrderNumber" + }, + { + "identity": "cda", + "map": "n/a" + } + ] + }, + { + "id": "Patient.photo", + "path": "Patient.photo", + "short": "Image of the patient", + "definition": "Image of the patient.", + "comment": "Guidelines:\n* Use id photos, not clinical photos.\n* Limit dimensions to thumbnail.\n* Keep byte count low to ease resource updates.", + "requirements": "Many EHR systems have the capability to capture an image of the patient. Fits with newer social media usage too.", + "min": 0, + "max": "*", + "base": { + "path": "Patient.photo", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "Attachment" + } + ], + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "v2", + "map": "OBX-5 - needs a profile" + }, + { + "identity": "rim", + "map": "player[classCode=PSN|ANM and determinerCode=INSTANCE]/desc" + }, + { + "identity": "cda", + "map": "n/a" + } + ] + }, + { + "id": "Patient.contact", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-explicit-type-name", + "valueString": "Contact" + } + ], + "path": "Patient.contact", + "short": "A contact party (e.g. guardian, partner, friend) for the patient", + "definition": "A contact party (e.g. guardian, partner, friend) for the patient.", + "comment": "Contact covers all kinds of contact parties: family members, business contacts, guardians, caregivers. Not applicable to register pedigree and family ties beyond use of having contact.", + "requirements": "Need to track people you can contact about the patient.", + "min": 0, + "max": "*", + "base": { + "path": "Patient.contact", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "BackboneElement" + } + ], + "constraint": [ + { + "key": "ele-1", + "severity": "error", + "human": "All FHIR elements must have a @value or children", + "expression": "hasValue() or (children().count() > id.count())", + "xpath": "@value|f:*|h:div", + "source": "Element" + }, + { + "key": "pat-1", + "severity": "error", + "human": "SHALL at least contain a contact's details or a reference to an organization", + "expression": "name.exists() or telecom.exists() or address.exists() or organization.exists()", + "xpath": "exists(f:name) or exists(f:telecom) or exists(f:address) or exists(f:organization)" + } + ], + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "rim", + "map": "player[classCode=PSN|ANM and determinerCode=INSTANCE]/scopedRole[classCode=CON]" + }, + { + "identity": "cda", + "map": "n/a" + } + ] + }, + { + "id": "Patient.contact.id", + "path": "Patient.contact.id", + "representation": [ + "xmlAttr" + ], + "short": "Unique id for inter-element referencing", + "definition": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + "min": 0, + "max": "1", + "base": { + "path": "Element.id", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "string" + } + ], + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "rim", + "map": "n/a" + } + ] + }, + { + "id": "Patient.contact.extension", + "path": "Patient.contact.extension", + "short": "Additional content defined by implementations", + "definition": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.", + "comment": "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "alias": [ + "extensions", + "user content" + ], + "min": 0, + "max": "*", + "base": { + "path": "Element.extension", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "Extension" + } + ], + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "rim", + "map": "n/a" + } + ] + }, + { + "id": "Patient.contact.modifierExtension", + "path": "Patient.contact.modifierExtension", + "short": "Extensions that cannot be ignored even if unrecognized", + "definition": "May be used to represent additional information that is not part of the basic definition of the element and that modifies the understanding of the element in which it is contained and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself).", + "comment": "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "requirements": "Modifier extensions allow for extensions that *cannot* be safely ignored to be clearly distinguished from the vast majority of extensions which can be safely ignored. This promotes interoperability by eliminating the need for implementers to prohibit the presence of extensions. For further information, see the [definition of modifier extensions](http://hl7.org/fhir/R4/extensibility.html#modifierExtension).", + "alias": [ + "extensions", + "user content", + "modifiers" + ], + "min": 0, + "max": "*", + "base": { + "path": "BackboneElement.modifierExtension", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "Extension" + } + ], + "isModifier": true, + "isModifierReason": "Modifier extensions are expected to modify the meaning or interpretation of the element that contains them", + "isSummary": true, + "mapping": [ + { + "identity": "rim", + "map": "N/A" + } + ] + }, + { + "id": "Patient.contact.relationship", + "path": "Patient.contact.relationship", + "short": "The kind of relationship", + "definition": "The nature of the relationship between the patient and the contact person.", + "requirements": "Used to determine which contact person is the most relevant to approach, depending on circumstances.", + "min": 0, + "max": "*", + "base": { + "path": "Patient.contact.relationship", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "CodeableConcept" + } + ], + "isModifier": false, + "isSummary": false, + "binding": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString": "ContactRelationship" + } + ], + "strength": "extensible", + "description": "The nature of the relationship between a patient and a contact person for that patient.", + "valueSet": "http://hl7.org/fhir/ValueSet/patient-contactrelationship" + }, + "mapping": [ + { + "identity": "v2", + "map": "NK1-7, NK1-3" + }, + { + "identity": "rim", + "map": "code" + }, + { + "identity": "cda", + "map": "n/a" + } + ] + }, + { + "id": "Patient.contact.name", + "path": "Patient.contact.name", + "short": "A name associated with the contact person", + "definition": "A name associated with the contact person.", + "requirements": "Contact persons need to be identified by name, but it is uncommon to need details about multiple other names for that contact person.", + "min": 0, + "max": "1", + "base": { + "path": "Patient.contact.name", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "HumanName" + } + ], + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "v2", + "map": "NK1-2" + }, + { + "identity": "rim", + "map": "name" + }, + { + "identity": "cda", + "map": "n/a" + } + ] + }, + { + "id": "Patient.contact.telecom", + "path": "Patient.contact.telecom", + "short": "A contact detail for the person", + "definition": "A contact detail for the person, e.g. a telephone number or an email address.", + "comment": "Contact may have multiple ways to be contacted with different uses or applicable periods. May need to have options for contacting the person urgently, and also to help with identification.", + "requirements": "People have (primary) ways to contact them in some way such as phone, email.", + "min": 0, + "max": "*", + "base": { + "path": "Patient.contact.telecom", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "ContactPoint" + } + ], + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "v2", + "map": "NK1-5, NK1-6, NK1-40" + }, + { + "identity": "rim", + "map": "telecom" + }, + { + "identity": "cda", + "map": "n/a" + } + ] + }, + { + "id": "Patient.contact.address", + "path": "Patient.contact.address", + "short": "Address for the contact person", + "definition": "Address for the contact person.", + "requirements": "Need to keep track where the contact person can be contacted per postal mail or visited.", + "min": 0, + "max": "1", + "base": { + "path": "Patient.contact.address", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "Address" + } + ], + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "v2", + "map": "NK1-4" + }, + { + "identity": "rim", + "map": "addr" + }, + { + "identity": "cda", + "map": "n/a" + } + ] + }, + { + "id": "Patient.contact.gender", + "path": "Patient.contact.gender", + "short": "male | female | other | unknown", + "definition": "Administrative Gender - the gender that the contact person is considered to have for administration and record keeping purposes.", + "requirements": "Needed to address the person correctly.", + "min": 0, + "max": "1", + "base": { + "path": "Patient.contact.gender", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "code" + } + ], + "isModifier": false, + "isSummary": false, + "binding": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString": "AdministrativeGender" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/elementdefinition-isCommonBinding", + "valueBoolean": true + } + ], + "strength": "required", + "description": "The gender of a person used for administrative purposes.", + "valueSet": "http://hl7.org/fhir/ValueSet/administrative-gender|4.0.0" + }, + "mapping": [ + { + "identity": "v2", + "map": "NK1-15" + }, + { + "identity": "rim", + "map": "player[classCode=PSN|ANM and determinerCode=INSTANCE]/administrativeGender" + }, + { + "identity": "cda", + "map": "n/a" + } + ] + }, + { + "id": "Patient.contact.organization", + "path": "Patient.contact.organization", + "short": "Organization that is associated with the contact", + "definition": "Organization on behalf of which the contact is acting or for which the contact is working.", + "requirements": "For guardians or business related contacts, the organization is relevant.", + "min": 0, + "max": "1", + "base": { + "path": "Patient.contact.organization", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "Reference", + "targetProfile": [ + "http://hl7.org/fhir/StructureDefinition/Organization" + ] + } + ], + "condition": [ + "pat-1" + ], + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "v2", + "map": "NK1-13, NK1-30, NK1-31, NK1-32, NK1-41" + }, + { + "identity": "rim", + "map": "scoper" + }, + { + "identity": "cda", + "map": "n/a" + } + ] + }, + { + "id": "Patient.contact.period", + "path": "Patient.contact.period", + "short": "The period during which this contact person or organization is valid to be contacted relating to this patient", + "definition": "The period during which this contact person or organization is valid to be contacted relating to this patient.", + "min": 0, + "max": "1", + "base": { + "path": "Patient.contact.period", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "Period" + } + ], + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "rim", + "map": "effectiveTime" + }, + { + "identity": "cda", + "map": "n/a" + } + ] + }, + { + "id": "Patient.communication", + "path": "Patient.communication", + "short": "A language which may be used to communicate with the patient about his or her health", + "definition": "A language which may be used to communicate with the patient about his or her health.", + "comment": "If no language is specified, this *implies* that the default local language is spoken. If you need to convey proficiency for multiple modes, then you need multiple Patient.Communication associations. For animals, language is not a relevant field, and should be absent from the instance. If the Patient does not speak the default local language, then the Interpreter Required Standard can be used to explicitly declare that an interpreter is required.", + "requirements": "If a patient does not speak the local language, interpreters may be required, so languages spoken and proficiency are important things to keep track of both for patient and other persons of interest.", + "min": 0, + "max": "*", + "base": { + "path": "Patient.communication", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "BackboneElement" + } + ], + "constraint": [ + { + "key": "ele-1", + "severity": "error", + "human": "All FHIR elements must have a @value or children", + "expression": "hasValue() or (children().count() > id.count())", + "xpath": "@value|f:*|h:div", + "source": "Element" + } + ], + "mustSupport": true, + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "rim", + "map": "LanguageCommunication" + }, + { + "identity": "cda", + "map": "patient.languageCommunication" + }, + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.communication" + } + ] + }, + { + "id": "Patient.communication.id", + "path": "Patient.communication.id", + "representation": [ + "xmlAttr" + ], + "short": "Unique id for inter-element referencing", + "definition": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + "min": 0, + "max": "1", + "base": { + "path": "Element.id", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "string" + } + ], + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "rim", + "map": "n/a" + } + ] + }, + { + "id": "Patient.communication.extension", + "path": "Patient.communication.extension", + "short": "Additional content defined by implementations", + "definition": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.", + "comment": "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "alias": [ + "extensions", + "user content" + ], + "min": 0, + "max": "*", + "base": { + "path": "Element.extension", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "Extension" + } + ], + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "rim", + "map": "n/a" + } + ] + }, + { + "id": "Patient.communication.modifierExtension", + "path": "Patient.communication.modifierExtension", + "short": "Extensions that cannot be ignored even if unrecognized", + "definition": "May be used to represent additional information that is not part of the basic definition of the element and that modifies the understanding of the element in which it is contained and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself).", + "comment": "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "requirements": "Modifier extensions allow for extensions that *cannot* be safely ignored to be clearly distinguished from the vast majority of extensions which can be safely ignored. This promotes interoperability by eliminating the need for implementers to prohibit the presence of extensions. For further information, see the [definition of modifier extensions](http://hl7.org/fhir/R4/extensibility.html#modifierExtension).", + "alias": [ + "extensions", + "user content", + "modifiers" + ], + "min": 0, + "max": "*", + "base": { + "path": "BackboneElement.modifierExtension", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "Extension" + } + ], + "isModifier": true, + "isModifierReason": "Modifier extensions are expected to modify the meaning or interpretation of the element that contains them", + "isSummary": true, + "mapping": [ + { + "identity": "rim", + "map": "N/A" + } + ] + }, + { + "id": "Patient.communication.language", + "path": "Patient.communication.language", + "short": "The language which can be used to communicate with the patient about his or her health", + "definition": "The ISO-639-1 alpha 2 code in lower case for the language, optionally followed by a hyphen and the ISO-3166-1 alpha 2 code for the region in upper case; e.g. \"en\" for English, or \"en-US\" for American English versus \"en-EN\" for England English.", + "comment": "The structure aa-BB with this exact casing is one the most widely used notations for locale. However not all systems actually code this but instead have it as free text. Hence CodeableConcept instead of code as the data type.", + "requirements": "Most systems in multilingual countries will want to convey language. Not all systems actually need the regional dialect.", + "min": 1, + "max": "1", + "base": { + "path": "Patient.communication.language", + "min": 1, + "max": "1" + }, + "type": [ + { + "code": "CodeableConcept" + } + ], + "mustSupport": true, + "isModifier": false, + "isSummary": false, + "binding": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet", + "valueCanonical": "http://hl7.org/fhir/us/core/ValueSet/simple-language" + } + ], + "strength": "extensible", + "valueSet": "http://hl7.org/fhir/us/core/ValueSet/simple-language" + }, + "mapping": [ + { + "identity": "v2", + "map": "PID-15, LAN-2" + }, + { + "identity": "rim", + "map": "player[classCode=PSN|ANM and determinerCode=INSTANCE]/languageCommunication/code" + }, + { + "identity": "cda", + "map": ".languageCode" + }, + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.communication.language" + } + ] + }, + { + "id": "Patient.communication.preferred", + "path": "Patient.communication.preferred", + "short": "Language preference indicator", + "definition": "Indicates whether or not the patient prefers this language (over other languages he masters up a certain level).", + "comment": "This language is specifically identified for communicating healthcare information.", + "requirements": "People that master multiple languages up to certain level may prefer one or more, i.e. feel more confident in communicating in a particular language making other languages sort of a fall back method.", + "min": 0, + "max": "1", + "base": { + "path": "Patient.communication.preferred", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "boolean" + } + ], + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "v2", + "map": "PID-15" + }, + { + "identity": "rim", + "map": "preferenceInd" + }, + { + "identity": "cda", + "map": ".preferenceInd" + } + ] + }, + { + "id": "Patient.generalPractitioner", + "path": "Patient.generalPractitioner", + "short": "Patient's nominated primary care provider", + "definition": "Patient's nominated care provider.", + "comment": "This may be the primary care provider (in a GP context), or it may be a patient nominated care manager in a community/disability setting, or even organization that will provide people to perform the care provider roles. It is not to be used to record Care Teams, these should be in a CareTeam resource that may be linked to the CarePlan or EpisodeOfCare resources.\nMultiple GPs may be recorded against the patient for various reasons, such as a student that has his home GP listed along with the GP at university during the school semesters, or a \"fly-in/fly-out\" worker that has the onsite GP also included with his home GP to remain aware of medical issues.\n\nJurisdictions may decide that they can profile this down to 1 if desired, or 1 per type.", + "alias": [ + "careProvider" + ], + "min": 0, + "max": "*", + "base": { + "path": "Patient.generalPractitioner", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "Reference", + "targetProfile": [ + "http://hl7.org/fhir/StructureDefinition/Organization", + "http://hl7.org/fhir/StructureDefinition/Practitioner", + "http://hl7.org/fhir/StructureDefinition/PractitionerRole" + ] + } + ], + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "v2", + "map": "PD1-4" + }, + { + "identity": "rim", + "map": "subjectOf.CareEvent.performer.AssignedEntity" + }, + { + "identity": "cda", + "map": "n/a" + } + ] + }, + { + "id": "Patient.managingOrganization", + "path": "Patient.managingOrganization", + "short": "Organization that is the custodian of the patient record", + "definition": "Organization that is the custodian of the patient record.", + "comment": "There is only one managing organization for a specific patient record. Other organizations will have their own Patient record, and may use the Link property to join the records together (or a Person resource which can include confidence ratings for the association).", + "requirements": "Need to know who recognizes this patient record, manages and updates it.", + "min": 0, + "max": "1", + "base": { + "path": "Patient.managingOrganization", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "Reference", + "targetProfile": [ + "http://hl7.org/fhir/StructureDefinition/Organization" + ] + } + ], + "isModifier": false, + "isSummary": true, + "mapping": [ + { + "identity": "rim", + "map": "scoper" + }, + { + "identity": "cda", + "map": ".providerOrganization" + } + ] + }, + { + "id": "Patient.link", + "path": "Patient.link", + "short": "Link to another patient resource that concerns the same actual person", + "definition": "Link to another patient resource that concerns the same actual patient.", + "comment": "There is no assumption that linked patient records have mutual links.", + "requirements": "There are multiple use cases: \n\n* Duplicate patient records due to the clerical errors associated with the difficulties of identifying humans consistently, and \n* Distribution of patient information across multiple servers.", + "min": 0, + "max": "*", + "base": { + "path": "Patient.link", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "BackboneElement" + } + ], + "constraint": [ + { + "key": "ele-1", + "severity": "error", + "human": "All FHIR elements must have a @value or children", + "expression": "hasValue() or (children().count() > id.count())", + "xpath": "@value|f:*|h:div", + "source": "Element" + } + ], + "isModifier": true, + "isModifierReason": "This element is labeled as a modifier because it might not be the main Patient resource, and the referenced patient should be used instead of this Patient record. This is when the link.type value is 'replaced-by'", + "isSummary": true, + "mapping": [ + { + "identity": "rim", + "map": "outboundLink" + }, + { + "identity": "cda", + "map": "n/a" + } + ] + }, + { + "id": "Patient.link.id", + "path": "Patient.link.id", + "representation": [ + "xmlAttr" + ], + "short": "Unique id for inter-element referencing", + "definition": "Unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + "min": 0, + "max": "1", + "base": { + "path": "Element.id", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "string" + } + ], + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "rim", + "map": "n/a" + } + ] + }, + { + "id": "Patient.link.extension", + "path": "Patient.link.extension", + "short": "Additional content defined by implementations", + "definition": "May be used to represent additional information that is not part of the basic definition of the element. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.", + "comment": "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "alias": [ + "extensions", + "user content" + ], + "min": 0, + "max": "*", + "base": { + "path": "Element.extension", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "Extension" + } + ], + "isModifier": false, + "isSummary": false, + "mapping": [ + { + "identity": "rim", + "map": "n/a" + } + ] + }, + { + "id": "Patient.link.modifierExtension", + "path": "Patient.link.modifierExtension", + "short": "Extensions that cannot be ignored even if unrecognized", + "definition": "May be used to represent additional information that is not part of the basic definition of the element and that modifies the understanding of the element in which it is contained and/or the understanding of the containing element's descendants. Usually modifier elements provide negation or qualification. To make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer can define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension. Applications processing a resource are required to check for modifier extensions.\n\nModifier extensions SHALL NOT change the meaning of any elements on Resource or DomainResource (including cannot change the meaning of modifierExtension itself).", + "comment": "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "requirements": "Modifier extensions allow for extensions that *cannot* be safely ignored to be clearly distinguished from the vast majority of extensions which can be safely ignored. This promotes interoperability by eliminating the need for implementers to prohibit the presence of extensions. For further information, see the [definition of modifier extensions](http://hl7.org/fhir/R4/extensibility.html#modifierExtension).", + "alias": [ + "extensions", + "user content", + "modifiers" + ], + "min": 0, + "max": "*", + "base": { + "path": "BackboneElement.modifierExtension", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "Extension" + } + ], + "isModifier": true, + "isModifierReason": "Modifier extensions are expected to modify the meaning or interpretation of the element that contains them", + "isSummary": true, + "mapping": [ + { + "identity": "rim", + "map": "N/A" + } + ] + }, + { + "id": "Patient.link.other", + "path": "Patient.link.other", + "short": "The other patient or related person resource that the link refers to", + "definition": "The other patient resource that the link refers to.", + "comment": "Referencing a RelatedPerson here removes the need to use a Person record to associate a Patient and RelatedPerson as the same individual.", + "min": 1, + "max": "1", + "base": { + "path": "Patient.link.other", + "min": 1, + "max": "1" + }, + "type": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/structuredefinition-hierarchy", + "valueBoolean": false + } + ], + "code": "Reference", + "targetProfile": [ + "http://hl7.org/fhir/StructureDefinition/Patient", + "http://hl7.org/fhir/StructureDefinition/RelatedPerson" + ] + } + ], + "isModifier": false, + "isSummary": true, + "mapping": [ + { + "identity": "v2", + "map": "PID-3, MRG-1" + }, + { + "identity": "rim", + "map": "id" + }, + { + "identity": "cda", + "map": "n/a" + } + ] + }, + { + "id": "Patient.link.type", + "path": "Patient.link.type", + "short": "replaced-by | replaces | refer | seealso", + "definition": "The type of link between this patient resource and another patient resource.", + "min": 1, + "max": "1", + "base": { + "path": "Patient.link.type", + "min": 1, + "max": "1" + }, + "type": [ + { + "code": "code" + } + ], + "isModifier": false, + "isSummary": true, + "binding": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/elementdefinition-bindingName", + "valueString": "LinkType" + } + ], + "strength": "required", + "description": "The type of link between this patient resource and another patient resource.", + "valueSet": "http://hl7.org/fhir/ValueSet/link-type|4.0.0" + }, + "mapping": [ + { + "identity": "rim", + "map": "typeCode" + }, + { + "identity": "cda", + "map": "n/a" + } + ] + } + ] + }, + "differential": { + "element": [ + { + "id": "Patient", + "path": "Patient", + "definition": "The US Core Patient Profile is based upon the core FHIR Patient Resource and designed to meet the applicable patient demographic data elements from the 2015 Edition Common Clinical Data Set.", + "mustSupport": false, + "isModifier": false, + "mapping": [ + { + "identity": "argonaut-dq-dstu2", + "map": "Patient" + } + ] + }, + { + "id": "Patient.extension:race", + "path": "Patient.extension", + "sliceName": "race", + "min": 0, + "max": "1", + "type": [ + { + "code": "Extension", + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race" + ] + } + ], + "mustSupport": true, + "isModifier": false, + "mapping": [ + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.extension" + } + ] + }, + { + "id": "Patient.extension:ethnicity", + "path": "Patient.extension", + "sliceName": "ethnicity", + "min": 0, + "max": "1", + "type": [ + { + "code": "Extension", + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity" + ] + } + ], + "mustSupport": true, + "isModifier": false, + "mapping": [ + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.extension" + } + ] + }, + { + "id": "Patient.extension:birthsex", + "path": "Patient.extension", + "sliceName": "birthsex", + "min": 0, + "max": "1", + "type": [ + { + "code": "Extension", + "profile": [ + "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex" + ] + } + ], + "mustSupport": true, + "isModifier": false, + "binding": { + "strength": "required", + "description": "Code for sex assigned at birth", + "valueSet": "http://hl7.org/fhir/us/core/ValueSet/birthsex" + }, + "mapping": [ + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.extension" + } + ] + }, + { + "id": "Patient.identifier", + "path": "Patient.identifier", + "min": 1, + "max": "*", + "type": [ + { + "code": "Identifier" + } + ], + "mustSupport": true, + "isModifier": false, + "mapping": [ + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.identifier" + } + ] + }, + { + "id": "Patient.identifier.system", + "path": "Patient.identifier.system", + "min": 1, + "max": "1", + "type": [ + { + "code": "uri" + } + ], + "mustSupport": true, + "isModifier": false, + "mapping": [ + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.identifier.system" + } + ] + }, + { + "id": "Patient.identifier.value", + "path": "Patient.identifier.value", + "short": "The value that is unique within the system.", + "min": 1, + "max": "1", + "type": [ + { + "code": "string" + } + ], + "mustSupport": true, + "isModifier": false, + "mapping": [ + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.identifier.value" + } + ] + }, + { + "id": "Patient.name", + "path": "Patient.name", + "min": 1, + "max": "*", + "type": [ + { + "code": "HumanName" + } + ], + "constraint": [ + { + "key": "us-core-8", + "severity": "error", + "human": "Patient.name.given or Patient.name.family or both SHALL be present", + "expression": "family.exists() or given.exists()", + "xpath": "f:given or f:family" + } + ], + "mustSupport": true, + "isModifier": false, + "mapping": [ + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.name" + } + ] + }, + { + "id": "Patient.name.family", + "path": "Patient.name.family", + "min": 0, + "max": "1", + "type": [ + { + "code": "string" + } + ], + "mustSupport": true, + "isModifier": false, + "mapping": [ + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.name.family" + } + ] + }, + { + "id": "Patient.name.given", + "path": "Patient.name.given", + "min": 0, + "max": "*", + "type": [ + { + "code": "string" + } + ], + "mustSupport": true, + "isModifier": false, + "mapping": [ + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.name.given" + } + ] + }, + { + "id": "Patient.telecom", + "path": "Patient.telecom", + "min": 0, + "max": "*", + "mustSupport": true, + "isModifier": false, + "mapping": [ + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.gender" + } + ] + }, + { + "id": "Patient.telecom.system", + "path": "Patient.telecom.system", + "min": 1, + "max": "1", + "mustSupport": true, + "isModifier": false, + "binding": { + "strength": "required", + "description": "Telecommunications form for contact point.", + "valueSet": "http://hl7.org/fhir/ValueSet/contact-point-system" + }, + "mapping": [ + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.gender" + } + ] + }, + { + "id": "Patient.telecom.value", + "path": "Patient.telecom.value", + "min": 1, + "max": "1", + "mustSupport": true, + "isModifier": false, + "mapping": [ + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.gender" + } + ] + }, + { + "id": "Patient.gender", + "path": "Patient.gender", + "min": 1, + "max": "1", + "type": [ + { + "code": "code" + } + ], + "mustSupport": true, + "isModifier": false, + "binding": { + "strength": "required", + "valueSet": "http://hl7.org/fhir/ValueSet/administrative-gender" + }, + "mapping": [ + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.gender" + } + ] + }, + { + "id": "Patient.birthDate", + "path": "Patient.birthDate", + "min": 0, + "max": "1", + "type": [ + { + "code": "date" + } + ], + "mustSupport": true, + "isModifier": false, + "mapping": [ + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.birthDate" + } + ] + }, + { + "id": "Patient.address", + "path": "Patient.address", + "min": 0, + "max": "*", + "mustSupport": true, + "isModifier": false, + "mapping": [ + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.birthDate" + } + ] + }, + { + "id": "Patient.address.line", + "path": "Patient.address.line", + "min": 0, + "max": "*", + "mustSupport": true, + "isModifier": false, + "mapping": [ + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.birthDate" + } + ] + }, + { + "id": "Patient.address.city", + "path": "Patient.address.city", + "min": 0, + "max": "1", + "mustSupport": true, + "isModifier": false, + "mapping": [ + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.birthDate" + } + ] + }, + { + "id": "Patient.address.state", + "path": "Patient.address.state", + "min": 0, + "max": "1", + "mustSupport": true, + "isModifier": false, + "binding": { + "strength": "extensible", + "description": "Two Letter USPS alphabetic codes.", + "valueSet": "http://hl7.org/fhir/us/core/ValueSet/us-core-usps-state" + }, + "mapping": [ + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.birthDate" + } + ] + }, + { + "id": "Patient.address.postalCode", + "path": "Patient.address.postalCode", + "short": "US Zip Codes", + "alias": [ + "Zip Code" + ], + "min": 0, + "max": "1", + "mustSupport": true, + "isModifier": false, + "mapping": [ + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.birthDate" + } + ] + }, + { + "id": "Patient.communication", + "path": "Patient.communication", + "min": 0, + "max": "*", + "mustSupport": true, + "isModifier": false, + "mapping": [ + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.communication" + } + ] + }, + { + "id": "Patient.communication.language", + "path": "Patient.communication.language", + "min": 1, + "max": "1", + "type": [ + { + "code": "CodeableConcept" + } + ], + "mustSupport": true, + "isModifier": false, + "binding": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/elementdefinition-maxValueSet", + "valueCanonical": "http://hl7.org/fhir/us/core/ValueSet/simple-language" + } + ], + "strength": "extensible", + "valueSet": "http://hl7.org/fhir/us/core/ValueSet/simple-language" + }, + "mapping": [ + { + "identity": "argonaut-dq-dstu2", + "map": "Patient.communication.language" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/test/unit/validator_test.rb b/test/unit/validator_test.rb new file mode 100644 index 000000000..e35f9de41 --- /dev/null +++ b/test/unit/validator_test.rb @@ -0,0 +1,90 @@ +require_relative '../test_helper' + +class ValidatorTest < Test::Unit::TestCase + FIXTURES_DIR = File.join('test', 'fixtures') + + class FakeValidator + def validate(resource) + return 'result' + end + end + + def setup + us_core_patient = File.join(FIXTURES_DIR, 'us_core', 'StructureDefinition-us-core-patient.json') + json = File.read(us_core_patient) + @us_core_patient_profile = FHIR.from_contents(json) + end + + def test_validator + validator = FHIR::Validator.new + assert validator.validator_modules.empty? + validator.register_validator_module(1) + assert validator.validator_modules.include? 1 + + validator.register_validator_module([3,4,5]) + assert validator.validator_modules.include? 4 + + assert validator.validator_modules.length == 4 + + validator = FHIR::Validator.new([1,2,3]) + validator.register_validator_module(1) # Should not add duplicate validators + assert validator.validator_modules.length == 3 + end + + def test_validator_validate + validator = FHIR::Validator.new + validator.register_validator_module([FakeValidator.new, FakeValidator.new]) + results = validator.validate(1) + assert results.length == 2 + end + + def test_us_core_validation + # Setup the Validator + profile_validator = FHIR::ProfileValidator.new(@us_core_patient_profile) + validator = FHIR::Validator.new(profile_validator) + + # Load the patient resource to be validated + example_name = 'invalid-Patient-example.json' + patient_record = File.join(FIXTURES_DIR, ['invalid_resources', example_name]) + json = File.read(patient_record) + resource = FHIR.from_contents(json) + + validator.show_skipped = true + results = validator.validate(resource) + assert !results.empty? + end + + # def test_profile_code_system_check + # # Clear any registered validators + # FHIR::StructureDefinition.clear_all_validates_vs + # FHIR::StructureDefinition.validates_vs "http://hl7.org/fhir/ValueSet/marital-status" do |coding| + # false # fails so that the code system validation happens + # end + # FHIR::StructureDefinition.validates_vs "http://hl7.org/fhir/v3/MaritalStatus" do |coding| + # true # no errors related to http://hl7.org/fhir/v3/MaritalStatus should be present + # end + # + # example_name = 'invalid-Patient-example.json' + # patient_record = File.join(FIXTURES_DIR, ['invalid_resources', example_name]) + # input_json = File.read(patient_record) + # resource = FHIR::Json.from_json(input_json) + # + # # validate against the declared profile + # profile = PROFILES[resource.meta.profile.first] + # profile = FHIR::Definitions.profile(resource.meta.profile.first) unless profile + # assert profile, "Failed to find profile: #{resource.meta.profile.first}" + # errors = profile.validate_resource(resource) + # errors << "Validated against #{resource.meta.profile.first}" unless errors.empty? + # + # assert !errors.empty?, 'Expected code valueset validation error.' + # assert errors.detect{|x| x.start_with?('Patient.maritalStatus has no codings from http://hl7.org/fhir/ValueSet/marital-status.')} + # assert !errors.detect{|x| x.start_with?('Patient.maritalStatus has no codings from http://hl7.org/fhir/v3/MaritalStatus.')} + # # check memory + # before = check_memory + # resource = nil + # profile = nil + # wait_for_gc + # after = check_memory + # assert_memory(before, after) + # end +end \ No newline at end of file From b311206bb85233ee00e20b32d5983d38c517a7a6 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Tue, 25 Jun 2019 13:50:39 -0400 Subject: [PATCH 02/80] linting --- .../validation/element_validator.rb | 4 +-- .../validation/profile_validator.rb | 26 ++++++++----------- lib/fhir_models/validation/validator.rb | 6 ++--- test/unit/validator_test.rb | 2 +- 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/lib/fhir_models/validation/element_validator.rb b/lib/fhir_models/validation/element_validator.rb index 6f68eb86c..1e608c056 100644 --- a/lib/fhir_models/validation/element_validator.rb +++ b/lib/fhir_models/validation/element_validator.rb @@ -30,7 +30,7 @@ def self.verify_element_cardinality(element, element_definition, current_path, s result end - def self.verify_data_type(element, element_definition, current_path, skip = false) + def self.verify_data_type(_element, element_definition, _current_path, _skip = false) element_definition.type.code.each do |datatype| if FHIR::RESOURCES.include? datatype @@ -38,4 +38,4 @@ def self.verify_data_type(element, element_definition, current_path, skip = fals end end end -end \ No newline at end of file +end diff --git a/lib/fhir_models/validation/profile_validator.rb b/lib/fhir_models/validation/profile_validator.rb index 23d85d00e..8ae12ce6e 100644 --- a/lib/fhir_models/validation/profile_validator.rb +++ b/lib/fhir_models/validation/profile_validator.rb @@ -175,7 +175,7 @@ def validate(resource) elms = v.send(meth) # More than one element where the FHIRPath needs indexing if elms.respond_to? :each_with_index - elms.each_with_index do |vv,kk| + elms.each_with_index do |vv, kk| digging["#{k}.#{meth}[#{kk}]"] = vv unless blank?(vv) end # Just One @@ -216,21 +216,17 @@ def validate(resource) # Handle Slices if elementdefinition.sliceName # Grab Extension slices - if elementdefinition.type.one? - if elementdefinition.type.first.code == 'Extension' - # Only select the elements which match the slice profile. - elements.select! do |k,v| - if indexed - v.url == elementdefinition.type.first.profile.first - else - v.select! {|vv| vv.url == elementdefinition.type.first.profile.first} - end - end + raise UnhandledSlice("Slice has more than one type. #{elementdefinition.id}") unless elementdefinition.type.one? + + raise UnhandledSlice("Slice type #{elementdefinition.type.code} is not handled. Only Extension slices are handled") unless elementdefinition.type.first.code == 'Extension' + + # Only select the elements which match the slice profile. + elements.select! do |_k, v| + if indexed + v.url == elementdefinition.type.first.profile.first else - raise UnhandledSlice("Slice type #{elementdefinition.type.code} is not handled. Only Extension slices are handled") + v.select! { |vv| vv.url == elementdefinition.type.first.profile.first } end - else - raise UnhandledSlice("Slice has more than one type. #{elementdefinition.id}") end end elements @@ -247,7 +243,7 @@ def register_vs_validator(valueset_uri, validator) # This Exception is for indicating types of slices that are not handled. # class UnhandledSlice < StandardError - def initialize(msg="Unhandled Slice") + def initialize(msg = 'Unhandled Slice') super(msg) end end diff --git a/lib/fhir_models/validation/validator.rb b/lib/fhir_models/validation/validator.rb index c65db7284..8beb03310 100644 --- a/lib/fhir_models/validation/validator.rb +++ b/lib/fhir_models/validation/validator.rb @@ -3,7 +3,7 @@ module FHIR # Implementation inspired by FHIR HAPI Validation class Validator attr_reader :validator_modules - attr_accessor :show_skipped + attr_reader :show_skipped # Creates a FHIR Validator # @@ -16,7 +16,7 @@ def initialize(validator_modules = []) # Register a validator_module # @param [#validate] validator_module def register_validator_module(validator_module) - @validator_modules.each {|validator| validator.show_skipped = @show_skipped} + @validator_modules.each { |validator| validator.show_skipped = @show_skipped if validator.respond_to?(:show_skipped) } add_validator_module(validator_module) end @@ -28,7 +28,7 @@ def validate(resource) def show_skipped=(skip) @show_skipped = skip - @validator_modules.each {|validator| validator.show_skipped = skip} + @validator_modules.each { |validator| validator.show_skipped = skip } end # Helper method for adding validator modules diff --git a/test/unit/validator_test.rb b/test/unit/validator_test.rb index e35f9de41..b13c918f6 100644 --- a/test/unit/validator_test.rb +++ b/test/unit/validator_test.rb @@ -49,7 +49,7 @@ def test_us_core_validation json = File.read(patient_record) resource = FHIR.from_contents(json) - validator.show_skipped = true + validator.show_skipped = false results = validator.validate(resource) assert !results.empty? end From 422deecb126d92b674548d451ce561f20dbe2f91 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Wed, 3 Jul 2019 17:06:19 -0400 Subject: [PATCH 03/80] refining type choice checking --- .../validation/profile_validator.rb | 42 +++++++++---------- spec/unit/validation/validation_api_spec.rb | 42 +++++++++++++++++++ 2 files changed, 61 insertions(+), 23 deletions(-) create mode 100644 spec/unit/validation/validation_api_spec.rb diff --git a/lib/fhir_models/validation/profile_validator.rb b/lib/fhir_models/validation/profile_validator.rb index 8ae12ce6e..dafa08e3a 100644 --- a/lib/fhir_models/validation/profile_validator.rb +++ b/lib/fhir_models/validation/profile_validator.rb @@ -86,27 +86,8 @@ def validate(resource) elements = retrieve_by_element_definition(element_definition, resource, false) result = [] - if skip && @show_skipped - result.push(*verify_element(nil, element_definition, p, skip)) - hierarchy[:results].push(*result) - @all_results.push(*result) - return - end - - # Normalize type choice elements to just the element for cardinality testing - if element_definition.path.end_with? '[x]' - mtelms = Hash.new([]) - elements.each do |k, v| - renorm = k.rpartition('.').first - mtelms["#{renorm}.#{element_definition.path.split('.').last}"].push(v) - end - elements = mtelms - end - - # Validate the Element - elements.each do |p, el| - result.push(*verify_element(el, element_definition, p, skip)) - end + # Get the Results + result.push(*verify_elements(elements, element_definition, skip)) # Save the validation results hierarchy[:results].push(*result) @@ -133,10 +114,25 @@ def validate(resource) # # @param resource [FHIR::Model] The resource to be validated # @param element_definition [FHIR::ElementDefinition] The ElementDefintion Resource which provides the validation criteria - private def verify_element(element, element_definition, current_path, skip = false) + private def verify_elements(elements, element_definition, skip = false) # This will hold the FHIR::ValidationResults from the various checks results = [] - results.push(ElementValidator.verify_element_cardinality(element, element_definition, current_path, skip)) + + # Normalize type choice elements to just the element for cardinality testing + if element_definition.path.end_with? '[x]' + mtelms = Hash.new([]) + elements.each do |k, v| + renorm = k.rpartition('.').first + mtelms["#{renorm}.#{element_definition.path.split('.').last}"].push(v) + end + mtelms.each do |p, el| + results.push(ElementValidator.verify_element_cardinality(el, element_definition, p, skip)) + end + end + + elements.each do |p, el| + results.push(ElementValidator.verify_element_cardinality(el, element_definition, p, skip)) + end results end diff --git a/spec/unit/validation/validation_api_spec.rb b/spec/unit/validation/validation_api_spec.rb new file mode 100644 index 000000000..c7c937563 --- /dev/null +++ b/spec/unit/validation/validation_api_spec.rb @@ -0,0 +1,42 @@ +describe 'Profile Resource Validation' do + FIXTURES_DIR = File.join('test', 'fixtures') + + let(:validator) {FHIR::Validator.new} + + it 'initially has no validator modules' do + expect(validator.validator_modules).to be_empty + end + + context 'with US Core Patient Profile Validator' do + let(:us_core_profile_validator) do + us_core_patient = File.join(FIXTURES_DIR, 'us_core', 'StructureDefinition-us-core-patient.json') + json = File.read(us_core_patient) + FHIR::ProfileValidator.new(FHIR.from_contents(json)) + end + + let(:patient_resource) do + patient_record = File.join(FIXTURES_DIR, ['invalid_resources', 'invalid-Patient-example.json']) + json = File.read(patient_record) + FHIR.from_contents(json) + end + + before { validator.register_validator_module(us_core_profile_validator) } + + it '#validate' do + results = validator.validate(patient_resource) + expect(results).to_not be_empty + end + + it 'checks element cardinality' do + results = validator.validate(patient_resource) + cardinality_results = results.select { |x| x.validation_type == :cardinality } + expect(cardinality_results).to_not be_empty + end + + it 'skips checking the cardinality of the root element' do + results = validator.validate(patient_resource) + cardinality_results = results.select { |x| x.is_successful == :skipped } + expect(cardinality_results).to_not be_empty + end + end +end \ No newline at end of file From 1f188c6ae15e2fc1ce27ebd4d18115a2c4cd1af3 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Wed, 3 Jul 2019 17:19:56 -0400 Subject: [PATCH 04/80] check way to many datatypes --- .../validation/element_validator.rb | 35 ++++++++++++++++--- .../validation/profile_validator.rb | 4 ++- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/lib/fhir_models/validation/element_validator.rb b/lib/fhir_models/validation/element_validator.rb index 1e608c056..7669e85af 100644 --- a/lib/fhir_models/validation/element_validator.rb +++ b/lib/fhir_models/validation/element_validator.rb @@ -30,12 +30,39 @@ def self.verify_element_cardinality(element, element_definition, current_path, s result end - def self.verify_data_type(_element, element_definition, _current_path, _skip = false) - element_definition.type.code.each do |datatype| - if FHIR::RESOURCES.include? datatype + # Verify the individual FHIR Data Types + # + # FHIR Resources, Profiles and the StructureDefinitions are made up of FHIR Data Types. + # There are two kinds of structures that fall under the FHIR Data Types: complex-type and primitive-type. + # The snapshot of a resource does not contain the element definitions associated with primitive-type or complex-type + # structures it is composed of. + # + # This test validates + # + # https://www.hl7.org/fhir/datatypes.html + def self.verify_data_type(element, element_definition, current_path, skip = false) + # TODO: Need to update element path to reflect that they are nested in a parent + if skip || element_definition.type.empty? # Root Elements do not have a type - end + result = FHIR::ValidationResult.new + result.element_definition = element_definition + result.validation_type = :datatype + result.is_successful = :skipped + result.element_path = current_path || element_definition.path + return result end + + # Get the type + type_code = if element_definition.type.one? + element_definition.type.first.code + else + element_definition.type.find do |datatype| + /[^.]+$/.match(element_definition.path.gsub('[x]', datatype.capitalize)) == /[^.]+$/.match(current_path) + end + end + type_def = FHIR::Definitions.type_definition(type_code) || FHIR::Definitions.resource_definition(type_code) + type_validator = FHIR::ProfileValidator.new(type_def) + type_validator.validate(element) end end end diff --git a/lib/fhir_models/validation/profile_validator.rb b/lib/fhir_models/validation/profile_validator.rb index dafa08e3a..465da6625 100644 --- a/lib/fhir_models/validation/profile_validator.rb +++ b/lib/fhir_models/validation/profile_validator.rb @@ -132,6 +132,8 @@ def validate(resource) elements.each do |p, el| results.push(ElementValidator.verify_element_cardinality(el, element_definition, p, skip)) + nilskip = el.nil? + results.push(*ElementValidator.verify_data_type(el, element_definition, p, skip || nilskip)) end results end @@ -187,7 +189,7 @@ def validate(resource) # If we don't want to index the last element (useful for testing cardinality) not_indexed = {} desired_elements.each do |k, v| - elms = v.send(last) + elms = v.send(last) if v.is_a? FHIR::Model # FHIR Primitives are not modeled and will throw NoMethod Error not_indexed["#{k}.#{last}"] = elms end not_indexed From 0632c99d4b2f17d962a9d55f71e1b8487d7408f0 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Thu, 4 Jul 2019 21:21:37 -0400 Subject: [PATCH 05/80] check elements against their type --- .../validation/element_validator.rb | 22 +- .../validation/profile_validator.rb | 30 +- .../StructureDefinition-us-core-race.json | 886 +++++++++++++++++- 3 files changed, 928 insertions(+), 10 deletions(-) diff --git a/lib/fhir_models/validation/element_validator.rb b/lib/fhir_models/validation/element_validator.rb index 7669e85af..32568cb12 100644 --- a/lib/fhir_models/validation/element_validator.rb +++ b/lib/fhir_models/validation/element_validator.rb @@ -43,7 +43,6 @@ def self.verify_element_cardinality(element, element_definition, current_path, s def self.verify_data_type(element, element_definition, current_path, skip = false) # TODO: Need to update element path to reflect that they are nested in a parent if skip || element_definition.type.empty? # Root Elements do not have a type - result = FHIR::ValidationResult.new result.element_definition = element_definition result.validation_type = :datatype @@ -57,12 +56,29 @@ def self.verify_data_type(element, element_definition, current_path, skip = fals element_definition.type.first.code else element_definition.type.find do |datatype| - /[^.]+$/.match(element_definition.path.gsub('[x]', datatype.capitalize)) == /[^.]+$/.match(current_path) + /[^.]+$/.match(element_definition.path.gsub('[x]', datatype.code.capitalize)) == /[^.]+$/.match(current_path) end end type_def = FHIR::Definitions.type_definition(type_code) || FHIR::Definitions.resource_definition(type_code) + if type_def.nil? + result = FHIR::ValidationResult.new + result.element_definition = element_definition + result.validation_type = :datatype + result.is_successful = :skipped + result.element_path = current_path || element_definition.path + return result + end type_validator = FHIR::ProfileValidator.new(type_def) - type_validator.validate(element) + results = type_validator.validate(element) + + results.each { |res| res.element_path = res.element_path.gsub(/^([^.]+)/, current_path) } + end + + # Error for Unknown Types + class UnknownType < StandardError + def initialize(msg = "Unknown TypeCode") + super(msg) + end end end end diff --git a/lib/fhir_models/validation/profile_validator.rb b/lib/fhir_models/validation/profile_validator.rb index 465da6625..a0c0b08b1 100644 --- a/lib/fhir_models/validation/profile_validator.rb +++ b/lib/fhir_models/validation/profile_validator.rb @@ -132,8 +132,17 @@ def validate(resource) elements.each do |p, el| results.push(ElementValidator.verify_element_cardinality(el, element_definition, p, skip)) - nilskip = el.nil? - results.push(*ElementValidator.verify_data_type(el, element_definition, p, skip || nilskip)) + # Don't validate an element that doesn't exist + unless blank?(el) + # If there are multiple elements (like exte) + if el.is_a? Array + el.each_with_index do |v, k| + results.push(*ElementValidator.verify_data_type(v, element_definition, "#{p}[#{k}]", skip)) + end + else + results.push(*ElementValidator.verify_data_type(el, element_definition, p, skip)) + end + end end results end @@ -219,12 +228,21 @@ def validate(resource) raise UnhandledSlice("Slice type #{elementdefinition.type.code} is not handled. Only Extension slices are handled") unless elementdefinition.type.first.code == 'Extension' # Only select the elements which match the slice profile. - elements.select! do |_k, v| - if indexed + if indexed + # Elements are already indexed + elements.select! do |_k, v| v.url == elementdefinition.type.first.profile.first - else - v.select! { |vv| vv.url == elementdefinition.type.first.profile.first } end + else + indexed_elements = {} + elements.each do |k, v| + v.each_with_index do |vv, kk| + if vv.url == elementdefinition.type.first.profile.first + indexed_elements["#{k}[#{kk}]"] = vv + end + end + end + elements = indexed_elements end end elements diff --git a/test/fixtures/us_core/StructureDefinition-us-core-race.json b/test/fixtures/us_core/StructureDefinition-us-core-race.json index 3b97111d6..cdb4ef391 100644 --- a/test/fixtures/us_core/StructureDefinition-us-core-race.json +++ b/test/fixtures/us_core/StructureDefinition-us-core-race.json @@ -1 +1,885 @@ -{"resourceType":"StructureDefinition","id":"us-core-race","text":{"status":"generated","div":"
\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n
NameFlagsCard.TypeDescription & Constraints\"doco\"
\".\"\".\" Extension 0..1US Core Race Extension
\".\"\".\"\".\" extension S0..5ExtensionAmerican Indian or Alaska Native|Asian|Black or African American|Native Hawaiian or Other Pacific Islander|White
\".\"\".\"\".\"\".\" url 1..1uri"ombCategory"
\".\"\".\"\".\"\".\" valueCoding 1..1CodingBinding: OMB Race Categories (required)
\".\"\".\"\".\" extension 0..*ExtensionExtended race codes
\".\"\".\"\".\"\".\" url 1..1uri"detailed"
\".\"\".\"\".\"\".\" valueCoding 1..1CodingBinding: US-Core Detailed Race (required)
\".\"\".\"\".\" extension S1..1ExtensionRace Text
\".\"\".\"\".\"\".\" url 1..1uri"text"
\".\"\".\"\".\"\".\" valueString 1..1string
\".\"\".\"\".\" url 1..1"http://hl7.org/fhir/us/core/StructureDefinition/us-core-race"

\"doco\" Documentation for this format
"},"url":"http://hl7.org/fhir/us/core/StructureDefinition/us-core-race","version":"4.0.0","name":"UsCoreRaceExtension","title":"US Core Race Extension","status":"active","date":"2016-08-01T00:00:00+00:00","publisher":"HL7 US Realm Steering Committee","contact":[{"telecom":[{"system":"url","value":"http://www.healthit.gov"}]}],"description":"Concepts classifying the person into a named category of humans sharing common history, traits, geographical origin or nationality. The race codes used to represent these concepts are based upon the [CDC Race and Ethnicity Code Set Version 1.0](http://www.cdc.gov/phin/resources/vocabulary/index.html) which includes over 900 concepts for representing race and ethnicity of which 921 reference race. The race concepts are grouped by and pre-mapped to the 5 OMB race categories: - American Indian or Alaska Native - Asian - Black or African American - Native Hawaiian or Other Pacific Islander - White.","jurisdiction":[{"coding":[{"system":"urn:iso:std:iso:3166","code":"US","display":"United States of America"}]}],"purpose":"Complies with 2015 Edition Common Clinical Data Set for patient race.","fhirVersion":"4.0.0","mapping":[{"identity":"rim","uri":"http://hl7.org/v3","name":"RIM Mapping"}],"kind":"complex-type","abstract":false,"context":[{"type":"element","expression":"Patient"}],"type":"Extension","baseDefinition":"http://hl7.org/fhir/StructureDefinition/Extension","derivation":"constraint","snapshot":{"element":[{"id":"Extension:race","path":"Extension","short":"US Core Race Extension","definition":"Concepts classifying the person into a named category of humans sharing common history, traits, geographical origin or nationality. The race codes used to represent these concepts are based upon the [CDC Race and Ethnicity Code Set Version 1.0](http://www.cdc.gov/phin/resources/vocabulary/index.html) which includes over 900 concepts for representing race and ethnicity of which 921 reference race. The race concepts are grouped by and pre-mapped to the 5 OMB race categories: - American Indian or Alaska Native - Asian - Black or African American - Native Hawaiian or Other Pacific Islander - White.","min":0,"max":"1","base":{"path":"Extension","min":0,"max":"*"},"condition":["ele-1"],"constraint":[{"key":"ele-1","severity":"error","human":"All FHIR elements must have a @value or children","expression":"children().count() > id.count()","xpath":"@value|f:*|h:div","source":"Element"},{"key":"ext-1","severity":"error","human":"Must have either extensions or value[x], not both","expression":"extension.exists() != value.exists()","xpath":"exists(f:extension)!=exists(f:*[starts-with(local-name(.), 'value')])","source":"Extension"}],"isModifier":false},{"id":"Extension:race.id","path":"Extension.id","representation":["xmlAttr"],"short":"xml:id (or equivalent in JSON)","definition":"unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.","min":0,"max":"1","base":{"path":"Element.id","min":0,"max":"1"},"type":[{"code":"string"}],"mapping":[{"identity":"rim","map":"n/a"}]},{"id":"Extension:race.extension","path":"Extension.extension","slicing":{"id":"1","discriminator":[{"type":"value","path":"url"}],"ordered":false,"rules":"open"},"short":"Extension","definition":"An Extension","min":0,"max":"*","base":{"path":"Element.extension","min":0,"max":"*"},"type":[{"code":"Extension"}]},{"id":"Extension:race.extension:ombcategory","path":"Extension.extension","sliceName":"ombCategory","short":"American Indian or Alaska Native|Asian|Black or African American|Native Hawaiian or Other Pacific Islander|White","definition":"The 5 race category codes according to the [OMB Standards for Maintaining, Collecting, and Presenting Federal Data on Race and Ethnicity, Statistical Policy Directive No. 15, as revised, October 30, 1997](https://www.whitehouse.gov/omb/fedreg_1997standards).","min":0,"max":"5","base":{"path":"Element.extension","min":0,"max":"*"},"type":[{"code":"Extension"}],"mustSupport":true,"isModifier":false,"mapping":[{"identity":"iso11179","map":"/ClinicalDocument/recordTarget/patientRole/patient/raceCode"}]},{"id":"Extension:race.extension:ombcategory.id","path":"Extension.extension.id","representation":["xmlAttr"],"short":"xml:id (or equivalent in JSON)","definition":"unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.","min":0,"max":"1","base":{"path":"Element.id","min":0,"max":"1"},"type":[{"code":"string"}],"mapping":[{"identity":"rim","map":"n/a"}]},{"id":"Extension:race.extension:ombcategory.extension","path":"Extension.extension.extension","short":"Additional Content defined by implementations","definition":"May be used to represent additional information that is not part of the basic definition of the element. In order to make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer is allowed to define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.","comment":"There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.","alias":["extensions","user content"],"min":0,"max":"0","base":{"path":"Element.extension","min":0,"max":"*"},"type":[{"code":"Extension"}],"mapping":[{"identity":"rim","map":"n/a"}]},{"id":"Extension:race.extension:ombcategory.url","path":"Extension.extension.url","representation":["xmlAttr"],"short":"identifies the meaning of the extension","definition":"Source of the definition for the extension code - a logical name or a URL.","comment":"The definition may point directly to a computable or human-readable definition of the extensibility codes, or it may be a logical URI as declared in some other specification. The definition SHALL be a URI for the Structure Definition defining the extension.","min":1,"max":"1","base":{"path":"Extension.url","min":1,"max":"1"},"type":[{"code":"uri"}],"fixedUri":"ombCategory","mapping":[{"identity":"rim","map":"N/A"}]},{"id":"Extension:race.extension:ombcategory.valueCoding","path":"Extension.extension.valueCoding","short":"Value of extension","definition":"Value of extension - may be a resource or one of a constrained set of the data types (see Extensibility in the spec for list).","min":1,"max":"1","base":{"path":"Extension.value[x]","min":0,"max":"1"},"type":[{"code":"Coding"}],"binding":{"strength":"required","description":"The 5 race category codes according to the [OMB Standards for Maintaining, Collecting, and Presenting Federal Data on Race and Ethnicity, Statistical Policy Directive No. 15, as revised, October 30, 1997](https://www.whitehouse.gov/omb/fedreg_1997standards).","valueSet":"http://hl7.org/fhir/us/core/ValueSet/omb-race-category"},"mapping":[{"identity":"rim","map":"N/A"}]},{"id":"Extension:race.extension:detailed","path":"Extension.extension","sliceName":"detailed","short":"Extended race codes","definition":"The 900+ CDC race codes that are grouped under one of the 5 OMB race category codes:.","min":0,"max":"*","base":{"path":"Element.extension","min":0,"max":"*"},"type":[{"code":"Extension"}],"isModifier":false,"mapping":[{"identity":"iso11179","map":"/ClinicalDocument/recordTarget/patientRole/patient/sdtc:raceCode"}]},{"id":"Extension:race.extension:detailed.id","path":"Extension.extension.id","representation":["xmlAttr"],"short":"xml:id (or equivalent in JSON)","definition":"unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.","min":0,"max":"1","base":{"path":"Element.id","min":0,"max":"1"},"type":[{"code":"string"}],"mapping":[{"identity":"rim","map":"n/a"}]},{"id":"Extension:race.extension:detailed.extension","path":"Extension.extension.extension","short":"Additional Content defined by implementations","definition":"May be used to represent additional information that is not part of the basic definition of the element. In order to make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer is allowed to define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.","comment":"There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.","alias":["extensions","user content"],"min":0,"max":"0","base":{"path":"Element.extension","min":0,"max":"*"},"type":[{"code":"Extension"}],"mapping":[{"identity":"rim","map":"n/a"}]},{"id":"Extension:race.extension:detailed.url","path":"Extension.extension.url","representation":["xmlAttr"],"short":"identifies the meaning of the extension","definition":"Source of the definition for the extension code - a logical name or a URL.","comment":"The definition may point directly to a computable or human-readable definition of the extensibility codes, or it may be a logical URI as declared in some other specification. The definition SHALL be a URI for the Structure Definition defining the extension.","min":1,"max":"1","base":{"path":"Extension.url","min":1,"max":"1"},"type":[{"code":"uri"}],"fixedUri":"detailed","mapping":[{"identity":"rim","map":"N/A"}]},{"id":"Extension:race.extension:detailed.valueCoding","path":"Extension.extension.valueCoding","short":"Value of extension","definition":"Value of extension - may be a resource or one of a constrained set of the data types (see Extensibility in the spec for list).","min":1,"max":"1","base":{"path":"Extension.value[x]","min":0,"max":"1"},"type":[{"code":"Coding"}],"binding":{"strength":"required","description":"The [900+ CDC Race codes](http://www.cdc.gov/phin/resources/vocabulary/index.html) that are grouped under one of the 5 OMB race category codes.","valueSet":"http://hl7.org/fhir/us/core/ValueSet/detailed-race"},"mapping":[{"identity":"rim","map":"N/A"}]},{"id":"Extension:race.extension:text","path":"Extension.extension","sliceName":"text","short":"Race Text","definition":"Plain text representation of the race concept(s).","min":1,"max":"1","base":{"path":"Element.extension","min":0,"max":"*"},"type":[{"code":"Extension"}],"mustSupport":true,"isModifier":false},{"id":"Extension:race.extension:text.id","path":"Extension.extension.id","representation":["xmlAttr"],"short":"xml:id (or equivalent in JSON)","definition":"unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.","min":0,"max":"1","base":{"path":"Element.id","min":0,"max":"1"},"type":[{"code":"string"}],"mapping":[{"identity":"rim","map":"n/a"}]},{"id":"Extension:race.extension:text.extension","path":"Extension.extension.extension","short":"Additional Content defined by implementations","definition":"May be used to represent additional information that is not part of the basic definition of the element. In order to make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer is allowed to define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.","comment":"There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.","alias":["extensions","user content"],"min":0,"max":"0","base":{"path":"Element.extension","min":0,"max":"*"},"type":[{"code":"Extension"}],"mapping":[{"identity":"rim","map":"n/a"}]},{"id":"Extension:race.extension:text.url","path":"Extension.extension.url","representation":["xmlAttr"],"short":"identifies the meaning of the extension","definition":"Source of the definition for the extension code - a logical name or a URL.","comment":"The definition may point directly to a computable or human-readable definition of the extensibility codes, or it may be a logical URI as declared in some other specification. The definition SHALL be a URI for the Structure Definition defining the extension.","min":1,"max":"1","base":{"path":"Extension.url","min":1,"max":"1"},"type":[{"code":"uri"}],"fixedUri":"text","mapping":[{"identity":"rim","map":"N/A"}]},{"id":"Extension:race.extension:text.valueString","path":"Extension.extension.valueString","short":"Value of extension","definition":"Value of extension - may be a resource or one of a constrained set of the data types (see Extensibility in the spec for list).","min":1,"max":"1","base":{"path":"Extension.value[x]","min":0,"max":"1"},"type":[{"code":"string"}],"mapping":[{"identity":"rim","map":"N/A"}]},{"id":"Extension:race.url","path":"Extension.url","representation":["xmlAttr"],"short":"identifies the meaning of the extension","definition":"Source of the definition for the extension code - a logical name or a URL.","comment":"The definition may point directly to a computable or human-readable definition of the extensibility codes, or it may be a logical URI as declared in some other specification. The definition SHALL be a URI for the Structure Definition defining the extension.","min":1,"max":"1","base":{"path":"Extension.url","min":1,"max":"1"},"type":[{"code":"uri"}],"fixedUri":"http://hl7.org/fhir/us/core/StructureDefinition/us-core-race","mapping":[{"identity":"rim","map":"N/A"}]},{"id":"Extension:race.value[x]","path":"Extension.value[x]","short":"Value of extension","definition":"Value of extension - may be a resource or one of a constrained set of the data types (see Extensibility in the spec for list).","min":0,"max":"0","base":{"path":"Extension.value[x]","min":0,"max":"1"},"type":[{"code":"base64Binary"},{"code":"boolean"},{"code":"code"},{"code":"date"},{"code":"dateTime"},{"code":"decimal"},{"code":"id"},{"code":"instant"},{"code":"integer"},{"code":"markdown"},{"code":"oid"},{"code":"positiveInt"},{"code":"string"},{"code":"time"},{"code":"unsignedInt"},{"code":"uri"},{"code":"Address"},{"code":"Age"},{"code":"Annotation"},{"code":"Attachment"},{"code":"CodeableConcept"},{"code":"Coding"},{"code":"ContactPoint"},{"code":"Count"},{"code":"Distance"},{"code":"Duration"},{"code":"HumanName"},{"code":"Identifier"},{"code":"Money"},{"code":"Period"},{"code":"Quantity"},{"code":"Range"},{"code":"Ratio"},{"code":"Reference"},{"code":"SampledData"},{"code":"Signature"},{"code":"Timing"},{"code":"Meta"}],"mapping":[{"identity":"rim","map":"N/A"}]}]},"differential":{"element":[{"id":"Extension:race","path":"Extension","short":"US Core Race Extension","definition":"Concepts classifying the person into a named category of humans sharing common history, traits, geographical origin or nationality. The race codes used to represent these concepts are based upon the [CDC Race and Ethnicity Code Set Version 1.0](http://www.cdc.gov/phin/resources/vocabulary/index.html) which includes over 900 concepts for representing race and ethnicity of which 921 reference race. The race concepts are grouped by and pre-mapped to the 5 OMB race categories: - American Indian or Alaska Native - Asian - Black or African American - Native Hawaiian or Other Pacific Islander - White.","min":0,"max":"1","isModifier":false},{"id":"Extension:race.extension:ombcategory","path":"Extension.extension","sliceName":"ombCategory","short":"American Indian or Alaska Native|Asian|Black or African American|Native Hawaiian or Other Pacific Islander|White","definition":"The 5 race category codes according to the [OMB Standards for Maintaining, Collecting, and Presenting Federal Data on Race and Ethnicity, Statistical Policy Directive No. 15, as revised, October 30, 1997](https://www.whitehouse.gov/omb/fedreg_1997standards).","min":0,"max":"5","type":[{"code":"Extension"}],"mustSupport":true,"isModifier":false,"mapping":[{"identity":"iso11179","map":"/ClinicalDocument/recordTarget/patientRole/patient/raceCode"}]},{"id":"Extension:race.extension:ombcategory.url","path":"Extension.extension.url","min":1,"max":"1","type":[{"code":"uri"}],"fixedUri":"ombCategory"},{"id":"Extension:race.extension:ombcategory.valueCoding","path":"Extension.extension.valueCoding","min":1,"max":"1","type":[{"code":"Coding"}],"binding":{"strength":"required","description":"The 5 race category codes according to the [OMB Standards for Maintaining, Collecting, and Presenting Federal Data on Race and Ethnicity, Statistical Policy Directive No. 15, as revised, October 30, 1997](https://www.whitehouse.gov/omb/fedreg_1997standards).","valueSet":"http://hl7.org/fhir/us/core/ValueSet/omb-race-category"}},{"id":"Extension:race.extension:detailed","path":"Extension.extension","sliceName":"detailed","short":"Extended race codes","definition":"The 900+ CDC race codes that are grouped under one of the 5 OMB race category codes:.","min":0,"max":"*","type":[{"code":"Extension"}],"isModifier":false,"mapping":[{"identity":"iso11179","map":"/ClinicalDocument/recordTarget/patientRole/patient/sdtc:raceCode"}]},{"id":"Extension:race.extension:detailed.url","path":"Extension.extension.url","min":1,"max":"1","type":[{"code":"uri"}],"fixedUri":"detailed"},{"id":"Extension:race.extension:detailed.valueCoding","path":"Extension.extension.valueCoding","min":1,"max":"1","type":[{"code":"Coding"}],"binding":{"strength":"required","description":"The [900+ CDC Race codes](http://www.cdc.gov/phin/resources/vocabulary/index.html) that are grouped under one of the 5 OMB race category codes.","valueSet":"http://hl7.org/fhir/us/core/ValueSet/detailed-race"}},{"id":"Extension:race.extension:text","path":"Extension.extension","sliceName":"text","short":"Race Text","definition":"Plain text representation of the race concept(s).","min":1,"max":"1","type":[{"code":"Extension"}],"mustSupport":true,"isModifier":false},{"id":"Extension:race.extension:text.url","path":"Extension.extension.url","min":1,"max":"1","type":[{"code":"uri"}],"fixedUri":"text"},{"id":"Extension:race.extension:text.valueString","path":"Extension.extension.valueString","min":1,"max":"1","type":[{"code":"string"}]},{"id":"Extension:race.url","path":"Extension.url","min":1,"max":"1","fixedUri":"http://hl7.org/fhir/us/core/StructureDefinition/us-core-race"}]}} \ No newline at end of file +{ + "resourceType": "StructureDefinition", + "id": "us-core-race", + "text": { + "status": "generated", + "div": "
\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n
NameFlagsCard.TypeDescription & Constraints\"doco\"
\".\"\".\" Extension 0..1US Core Race Extension
\".\"\".\"\".\" extension S0..5ExtensionAmerican Indian or Alaska Native|Asian|Black or African American|Native Hawaiian or Other Pacific Islander|White
\".\"\".\"\".\"\".\" url 1..1uri"ombCategory"
\".\"\".\"\".\"\".\" valueCoding 1..1CodingBinding: OMB Race Categories (required)
\".\"\".\"\".\" extension 0..*ExtensionExtended race codes
\".\"\".\"\".\"\".\" url 1..1uri"detailed"
\".\"\".\"\".\"\".\" valueCoding 1..1CodingBinding: US-Core Detailed Race (required)
\".\"\".\"\".\" extension S1..1ExtensionRace Text
\".\"\".\"\".\"\".\" url 1..1uri"text"
\".\"\".\"\".\"\".\" valueString 1..1string
\".\"\".\"\".\" url 1..1"http://hl7.org/fhir/us/core/StructureDefinition/us-core-race"

\"doco\" Documentation for this format
" + }, + "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", + "version": "4.0.0", + "name": "UsCoreRaceExtension", + "title": "US Core Race Extension", + "status": "active", + "date": "2016-08-01T00:00:00+00:00", + "publisher": "HL7 US Realm Steering Committee", + "contact": [ + { + "telecom": [ + { + "system": "url", + "value": "http://www.healthit.gov" + } + ] + } + ], + "description": "Concepts classifying the person into a named category of humans sharing common history, traits, geographical origin or nationality. The race codes used to represent these concepts are based upon the [CDC Race and Ethnicity Code Set Version 1.0](http://www.cdc.gov/phin/resources/vocabulary/index.html) which includes over 900 concepts for representing race and ethnicity of which 921 reference race. The race concepts are grouped by and pre-mapped to the 5 OMB race categories: - American Indian or Alaska Native - Asian - Black or African American - Native Hawaiian or Other Pacific Islander - White.", + "jurisdiction": [ + { + "coding": [ + { + "system": "urn:iso:std:iso:3166", + "code": "US", + "display": "United States of America" + } + ] + } + ], + "purpose": "Complies with 2015 Edition Common Clinical Data Set for patient race.", + "fhirVersion": "4.0.0", + "mapping": [ + { + "identity": "rim", + "uri": "http://hl7.org/v3", + "name": "RIM Mapping" + } + ], + "kind": "complex-type", + "abstract": false, + "context": [ + { + "type": "element", + "expression": "Patient" + } + ], + "type": "Extension", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Extension", + "derivation": "constraint", + "snapshot": { + "element": [ + { + "id": "Extension:race", + "path": "Extension", + "short": "US Core Race Extension", + "definition": "Concepts classifying the person into a named category of humans sharing common history, traits, geographical origin or nationality. The race codes used to represent these concepts are based upon the [CDC Race and Ethnicity Code Set Version 1.0](http://www.cdc.gov/phin/resources/vocabulary/index.html) which includes over 900 concepts for representing race and ethnicity of which 921 reference race. The race concepts are grouped by and pre-mapped to the 5 OMB race categories: - American Indian or Alaska Native - Asian - Black or African American - Native Hawaiian or Other Pacific Islander - White.", + "min": 0, + "max": "1", + "base": { + "path": "Extension", + "min": 0, + "max": "*" + }, + "condition": [ + "ele-1" + ], + "constraint": [ + { + "key": "ele-1", + "severity": "error", + "human": "All FHIR elements must have a @value or children", + "expression": "children().count() > id.count()", + "xpath": "@value|f:*|h:div", + "source": "Element" + }, + { + "key": "ext-1", + "severity": "error", + "human": "Must have either extensions or value[x], not both", + "expression": "extension.exists() != value.exists()", + "xpath": "exists(f:extension)!=exists(f:*[starts-with(local-name(.), 'value')])", + "source": "Extension" + } + ], + "isModifier": false + }, + { + "id": "Extension:race.id", + "path": "Extension.id", + "representation": [ + "xmlAttr" + ], + "short": "xml:id (or equivalent in JSON)", + "definition": "unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + "min": 0, + "max": "1", + "base": { + "path": "Element.id", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "string" + } + ], + "mapping": [ + { + "identity": "rim", + "map": "n/a" + } + ] + }, + { + "id": "Extension:race.extension", + "path": "Extension.extension", + "slicing": { + "id": "1", + "discriminator": [ + { + "type": "value", + "path": "url" + } + ], + "ordered": false, + "rules": "open" + }, + "short": "Extension", + "definition": "An Extension", + "min": 0, + "max": "*", + "base": { + "path": "Element.extension", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "Extension" + } + ] + }, + { + "id": "Extension:race.extension:ombcategory", + "path": "Extension.extension", + "sliceName": "ombCategory", + "short": "American Indian or Alaska Native|Asian|Black or African American|Native Hawaiian or Other Pacific Islander|White", + "definition": "The 5 race category codes according to the [OMB Standards for Maintaining, Collecting, and Presenting Federal Data on Race and Ethnicity, Statistical Policy Directive No. 15, as revised, October 30, 1997](https://www.whitehouse.gov/omb/fedreg_1997standards).", + "min": 0, + "max": "5", + "base": { + "path": "Element.extension", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "Extension" + } + ], + "mustSupport": true, + "isModifier": false, + "mapping": [ + { + "identity": "iso11179", + "map": "/ClinicalDocument/recordTarget/patientRole/patient/raceCode" + } + ] + }, + { + "id": "Extension:race.extension:ombcategory.id", + "path": "Extension.extension.id", + "representation": [ + "xmlAttr" + ], + "short": "xml:id (or equivalent in JSON)", + "definition": "unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + "min": 0, + "max": "1", + "base": { + "path": "Element.id", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "string" + } + ], + "mapping": [ + { + "identity": "rim", + "map": "n/a" + } + ] + }, + { + "id": "Extension:race.extension:ombcategory.extension", + "path": "Extension.extension.extension", + "short": "Additional Content defined by implementations", + "definition": "May be used to represent additional information that is not part of the basic definition of the element. In order to make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer is allowed to define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.", + "comment": "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "alias": [ + "extensions", + "user content" + ], + "min": 0, + "max": "0", + "base": { + "path": "Element.extension", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "Extension" + } + ], + "mapping": [ + { + "identity": "rim", + "map": "n/a" + } + ] + }, + { + "id": "Extension:race.extension:ombcategory.url", + "path": "Extension.extension.url", + "representation": [ + "xmlAttr" + ], + "short": "identifies the meaning of the extension", + "definition": "Source of the definition for the extension code - a logical name or a URL.", + "comment": "The definition may point directly to a computable or human-readable definition of the extensibility codes, or it may be a logical URI as declared in some other specification. The definition SHALL be a URI for the Structure Definition defining the extension.", + "min": 1, + "max": "1", + "base": { + "path": "Extension.url", + "min": 1, + "max": "1" + }, + "type": [ + { + "code": "uri" + } + ], + "fixedUri": "ombCategory", + "mapping": [ + { + "identity": "rim", + "map": "N/A" + } + ] + }, + { + "id": "Extension:race.extension:ombcategory.valueCoding", + "path": "Extension.extension.valueCoding", + "short": "Value of extension", + "definition": "Value of extension - may be a resource or one of a constrained set of the data types (see Extensibility in the spec for list).", + "min": 1, + "max": "1", + "base": { + "path": "Extension.value[x]", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "Coding" + } + ], + "binding": { + "strength": "required", + "description": "The 5 race category codes according to the [OMB Standards for Maintaining, Collecting, and Presenting Federal Data on Race and Ethnicity, Statistical Policy Directive No. 15, as revised, October 30, 1997](https://www.whitehouse.gov/omb/fedreg_1997standards).", + "valueSet": "http://hl7.org/fhir/us/core/ValueSet/omb-race-category" + }, + "mapping": [ + { + "identity": "rim", + "map": "N/A" + } + ] + }, + { + "id": "Extension:race.extension:detailed", + "path": "Extension.extension", + "sliceName": "detailed", + "short": "Extended race codes", + "definition": "The 900+ CDC race codes that are grouped under one of the 5 OMB race category codes:.", + "min": 0, + "max": "*", + "base": { + "path": "Element.extension", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "Extension" + } + ], + "isModifier": false, + "mapping": [ + { + "identity": "iso11179", + "map": "/ClinicalDocument/recordTarget/patientRole/patient/sdtc:raceCode" + } + ] + }, + { + "id": "Extension:race.extension:detailed.id", + "path": "Extension.extension.id", + "representation": [ + "xmlAttr" + ], + "short": "xml:id (or equivalent in JSON)", + "definition": "unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + "min": 0, + "max": "1", + "base": { + "path": "Element.id", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "string" + } + ], + "mapping": [ + { + "identity": "rim", + "map": "n/a" + } + ] + }, + { + "id": "Extension:race.extension:detailed.extension", + "path": "Extension.extension.extension", + "short": "Additional Content defined by implementations", + "definition": "May be used to represent additional information that is not part of the basic definition of the element. In order to make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer is allowed to define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.", + "comment": "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "alias": [ + "extensions", + "user content" + ], + "min": 0, + "max": "0", + "base": { + "path": "Element.extension", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "Extension" + } + ], + "mapping": [ + { + "identity": "rim", + "map": "n/a" + } + ] + }, + { + "id": "Extension:race.extension:detailed.url", + "path": "Extension.extension.url", + "representation": [ + "xmlAttr" + ], + "short": "identifies the meaning of the extension", + "definition": "Source of the definition for the extension code - a logical name or a URL.", + "comment": "The definition may point directly to a computable or human-readable definition of the extensibility codes, or it may be a logical URI as declared in some other specification. The definition SHALL be a URI for the Structure Definition defining the extension.", + "min": 1, + "max": "1", + "base": { + "path": "Extension.url", + "min": 1, + "max": "1" + }, + "type": [ + { + "code": "uri" + } + ], + "fixedUri": "detailed", + "mapping": [ + { + "identity": "rim", + "map": "N/A" + } + ] + }, + { + "id": "Extension:race.extension:detailed.valueCoding", + "path": "Extension.extension.valueCoding", + "short": "Value of extension", + "definition": "Value of extension - may be a resource or one of a constrained set of the data types (see Extensibility in the spec for list).", + "min": 1, + "max": "1", + "base": { + "path": "Extension.value[x]", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "Coding" + } + ], + "binding": { + "strength": "required", + "description": "The [900+ CDC Race codes](http://www.cdc.gov/phin/resources/vocabulary/index.html) that are grouped under one of the 5 OMB race category codes.", + "valueSet": "http://hl7.org/fhir/us/core/ValueSet/detailed-race" + }, + "mapping": [ + { + "identity": "rim", + "map": "N/A" + } + ] + }, + { + "id": "Extension:race.extension:text", + "path": "Extension.extension", + "sliceName": "text", + "short": "Race Text", + "definition": "Plain text representation of the race concept(s).", + "min": 1, + "max": "1", + "base": { + "path": "Element.extension", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "Extension" + } + ], + "mustSupport": true, + "isModifier": false + }, + { + "id": "Extension:race.extension:text.id", + "path": "Extension.extension.id", + "representation": [ + "xmlAttr" + ], + "short": "xml:id (or equivalent in JSON)", + "definition": "unique id for the element within a resource (for internal references). This may be any string value that does not contain spaces.", + "min": 0, + "max": "1", + "base": { + "path": "Element.id", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "string" + } + ], + "mapping": [ + { + "identity": "rim", + "map": "n/a" + } + ] + }, + { + "id": "Extension:race.extension:text.extension", + "path": "Extension.extension.extension", + "short": "Additional Content defined by implementations", + "definition": "May be used to represent additional information that is not part of the basic definition of the element. In order to make the use of extensions safe and manageable, there is a strict set of governance applied to the definition and use of extensions. Though any implementer is allowed to define an extension, there is a set of requirements that SHALL be met as part of the definition of the extension.", + "comment": "There can be no stigma associated with the use of extensions by any application, project, or standard - regardless of the institution or jurisdiction that uses or defines the extensions. The use of extensions is what allows the FHIR specification to retain a core level of simplicity for everyone.", + "alias": [ + "extensions", + "user content" + ], + "min": 0, + "max": "0", + "base": { + "path": "Element.extension", + "min": 0, + "max": "*" + }, + "type": [ + { + "code": "Extension" + } + ], + "mapping": [ + { + "identity": "rim", + "map": "n/a" + } + ] + }, + { + "id": "Extension:race.extension:text.url", + "path": "Extension.extension.url", + "representation": [ + "xmlAttr" + ], + "short": "identifies the meaning of the extension", + "definition": "Source of the definition for the extension code - a logical name or a URL.", + "comment": "The definition may point directly to a computable or human-readable definition of the extensibility codes, or it may be a logical URI as declared in some other specification. The definition SHALL be a URI for the Structure Definition defining the extension.", + "min": 1, + "max": "1", + "base": { + "path": "Extension.url", + "min": 1, + "max": "1" + }, + "type": [ + { + "code": "uri" + } + ], + "fixedUri": "text", + "mapping": [ + { + "identity": "rim", + "map": "N/A" + } + ] + }, + { + "id": "Extension:race.extension:text.valueString", + "path": "Extension.extension.valueString", + "short": "Value of extension", + "definition": "Value of extension - may be a resource or one of a constrained set of the data types (see Extensibility in the spec for list).", + "min": 1, + "max": "1", + "base": { + "path": "Extension.value[x]", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "string" + } + ], + "mapping": [ + { + "identity": "rim", + "map": "N/A" + } + ] + }, + { + "id": "Extension:race.url", + "path": "Extension.url", + "representation": [ + "xmlAttr" + ], + "short": "identifies the meaning of the extension", + "definition": "Source of the definition for the extension code - a logical name or a URL.", + "comment": "The definition may point directly to a computable or human-readable definition of the extensibility codes, or it may be a logical URI as declared in some other specification. The definition SHALL be a URI for the Structure Definition defining the extension.", + "min": 1, + "max": "1", + "base": { + "path": "Extension.url", + "min": 1, + "max": "1" + }, + "type": [ + { + "code": "uri" + } + ], + "fixedUri": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", + "mapping": [ + { + "identity": "rim", + "map": "N/A" + } + ] + }, + { + "id": "Extension:race.value[x]", + "path": "Extension.value[x]", + "short": "Value of extension", + "definition": "Value of extension - may be a resource or one of a constrained set of the data types (see Extensibility in the spec for list).", + "min": 0, + "max": "0", + "base": { + "path": "Extension.value[x]", + "min": 0, + "max": "1" + }, + "type": [ + { + "code": "base64Binary" + }, + { + "code": "boolean" + }, + { + "code": "code" + }, + { + "code": "date" + }, + { + "code": "dateTime" + }, + { + "code": "decimal" + }, + { + "code": "id" + }, + { + "code": "instant" + }, + { + "code": "integer" + }, + { + "code": "markdown" + }, + { + "code": "oid" + }, + { + "code": "positiveInt" + }, + { + "code": "string" + }, + { + "code": "time" + }, + { + "code": "unsignedInt" + }, + { + "code": "uri" + }, + { + "code": "Address" + }, + { + "code": "Age" + }, + { + "code": "Annotation" + }, + { + "code": "Attachment" + }, + { + "code": "CodeableConcept" + }, + { + "code": "Coding" + }, + { + "code": "ContactPoint" + }, + { + "code": "Count" + }, + { + "code": "Distance" + }, + { + "code": "Duration" + }, + { + "code": "HumanName" + }, + { + "code": "Identifier" + }, + { + "code": "Money" + }, + { + "code": "Period" + }, + { + "code": "Quantity" + }, + { + "code": "Range" + }, + { + "code": "Ratio" + }, + { + "code": "Reference" + }, + { + "code": "SampledData" + }, + { + "code": "Signature" + }, + { + "code": "Timing" + }, + { + "code": "Meta" + } + ], + "mapping": [ + { + "identity": "rim", + "map": "N/A" + } + ] + } + ] + }, + "differential": { + "element": [ + { + "id": "Extension:race", + "path": "Extension", + "short": "US Core Race Extension", + "definition": "Concepts classifying the person into a named category of humans sharing common history, traits, geographical origin or nationality. The race codes used to represent these concepts are based upon the [CDC Race and Ethnicity Code Set Version 1.0](http://www.cdc.gov/phin/resources/vocabulary/index.html) which includes over 900 concepts for representing race and ethnicity of which 921 reference race. The race concepts are grouped by and pre-mapped to the 5 OMB race categories: - American Indian or Alaska Native - Asian - Black or African American - Native Hawaiian or Other Pacific Islander - White.", + "min": 0, + "max": "1", + "isModifier": false + }, + { + "id": "Extension:race.extension:ombcategory", + "path": "Extension.extension", + "sliceName": "ombCategory", + "short": "American Indian or Alaska Native|Asian|Black or African American|Native Hawaiian or Other Pacific Islander|White", + "definition": "The 5 race category codes according to the [OMB Standards for Maintaining, Collecting, and Presenting Federal Data on Race and Ethnicity, Statistical Policy Directive No. 15, as revised, October 30, 1997](https://www.whitehouse.gov/omb/fedreg_1997standards).", + "min": 0, + "max": "5", + "type": [ + { + "code": "Extension" + } + ], + "mustSupport": true, + "isModifier": false, + "mapping": [ + { + "identity": "iso11179", + "map": "/ClinicalDocument/recordTarget/patientRole/patient/raceCode" + } + ] + }, + { + "id": "Extension:race.extension:ombcategory.url", + "path": "Extension.extension.url", + "min": 1, + "max": "1", + "type": [ + { + "code": "uri" + } + ], + "fixedUri": "ombCategory" + }, + { + "id": "Extension:race.extension:ombcategory.valueCoding", + "path": "Extension.extension.valueCoding", + "min": 1, + "max": "1", + "type": [ + { + "code": "Coding" + } + ], + "binding": { + "strength": "required", + "description": "The 5 race category codes according to the [OMB Standards for Maintaining, Collecting, and Presenting Federal Data on Race and Ethnicity, Statistical Policy Directive No. 15, as revised, October 30, 1997](https://www.whitehouse.gov/omb/fedreg_1997standards).", + "valueSet": "http://hl7.org/fhir/us/core/ValueSet/omb-race-category" + } + }, + { + "id": "Extension:race.extension:detailed", + "path": "Extension.extension", + "sliceName": "detailed", + "short": "Extended race codes", + "definition": "The 900+ CDC race codes that are grouped under one of the 5 OMB race category codes:.", + "min": 0, + "max": "*", + "type": [ + { + "code": "Extension" + } + ], + "isModifier": false, + "mapping": [ + { + "identity": "iso11179", + "map": "/ClinicalDocument/recordTarget/patientRole/patient/sdtc:raceCode" + } + ] + }, + { + "id": "Extension:race.extension:detailed.url", + "path": "Extension.extension.url", + "min": 1, + "max": "1", + "type": [ + { + "code": "uri" + } + ], + "fixedUri": "detailed" + }, + { + "id": "Extension:race.extension:detailed.valueCoding", + "path": "Extension.extension.valueCoding", + "min": 1, + "max": "1", + "type": [ + { + "code": "Coding" + } + ], + "binding": { + "strength": "required", + "description": "The [900+ CDC Race codes](http://www.cdc.gov/phin/resources/vocabulary/index.html) that are grouped under one of the 5 OMB race category codes.", + "valueSet": "http://hl7.org/fhir/us/core/ValueSet/detailed-race" + } + }, + { + "id": "Extension:race.extension:text", + "path": "Extension.extension", + "sliceName": "text", + "short": "Race Text", + "definition": "Plain text representation of the race concept(s).", + "min": 1, + "max": "1", + "type": [ + { + "code": "Extension" + } + ], + "mustSupport": true, + "isModifier": false + }, + { + "id": "Extension:race.extension:text.url", + "path": "Extension.extension.url", + "min": 1, + "max": "1", + "type": [ + { + "code": "uri" + } + ], + "fixedUri": "text" + }, + { + "id": "Extension:race.extension:text.valueString", + "path": "Extension.extension.valueString", + "min": 1, + "max": "1", + "type": [ + { + "code": "string" + } + ] + }, + { + "id": "Extension:race.url", + "path": "Extension.url", + "min": 1, + "max": "1", + "fixedUri": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race" + } + ] + } +} \ No newline at end of file From 0b92804561f50e66ea1f31c0f7b7fdae61c7dcc2 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Thu, 4 Jul 2019 23:34:44 -0400 Subject: [PATCH 06/80] guard against datatype validation of elements without a type --- lib/fhir_models/validation/element_validator.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/fhir_models/validation/element_validator.rb b/lib/fhir_models/validation/element_validator.rb index 32568cb12..6583bcbb5 100644 --- a/lib/fhir_models/validation/element_validator.rb +++ b/lib/fhir_models/validation/element_validator.rb @@ -41,8 +41,10 @@ def self.verify_element_cardinality(element, element_definition, current_path, s # # https://www.hl7.org/fhir/datatypes.html def self.verify_data_type(element, element_definition, current_path, skip = false) - # TODO: Need to update element path to reflect that they are nested in a parent - if skip || element_definition.type.empty? # Root Elements do not have a type + return unless element_definition.path.include? '.' # Root Elements do not have a type + + # Can't do this validation if there is no type. + if skip || element_definition.type.empty? result = FHIR::ValidationResult.new result.element_definition = element_definition result.validation_type = :datatype @@ -57,9 +59,11 @@ def self.verify_data_type(element, element_definition, current_path, skip = fals else element_definition.type.find do |datatype| /[^.]+$/.match(element_definition.path.gsub('[x]', datatype.code.capitalize)) == /[^.]+$/.match(current_path) - end + end.code end type_def = FHIR::Definitions.type_definition(type_code) || FHIR::Definitions.resource_definition(type_code) + + # If we are missing the Structure Definition needed to do the validation. if type_def.nil? result = FHIR::ValidationResult.new result.element_definition = element_definition From 7c273af308c62f72b2b6c7f0dc15b181f4dde6fe Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Wed, 10 Jul 2019 14:45:12 -0400 Subject: [PATCH 07/80] add profile url to result --- lib/fhir_models/validation/profile_validator.rb | 4 +++- lib/fhir_models/validation/validation_result.rb | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/fhir_models/validation/profile_validator.rb b/lib/fhir_models/validation/profile_validator.rb index a0c0b08b1..ffb6baffa 100644 --- a/lib/fhir_models/validation/profile_validator.rb +++ b/lib/fhir_models/validation/profile_validator.rb @@ -87,7 +87,9 @@ def validate(resource) result = [] # Get the Results - result.push(*verify_elements(elements, element_definition, skip)) + profile_results = verify_elements(elements, element_definition, skip) + profile_results.each { |res| res.profile ||= @profile.url } + result.push(*profile_results) # Save the validation results hierarchy[:results].push(*result) diff --git a/lib/fhir_models/validation/validation_result.rb b/lib/fhir_models/validation/validation_result.rb index a24bd8fae..61480a6c1 100644 --- a/lib/fhir_models/validation/validation_result.rb +++ b/lib/fhir_models/validation/validation_result.rb @@ -7,6 +7,7 @@ class ValidationResult attr_accessor :validation_type attr_accessor :element_definition_id attr_accessor :element_path + attr_accessor :profile # Returns the validation result as an OperationOutcome # From 4dc0216ae852cac902dde8b5115ede21ac4cf81d Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Thu, 25 Jul 2019 10:38:42 -0400 Subject: [PATCH 08/80] refactoring element validators --- lib/fhir_models.rb | 2 +- .../fhir_ext/structure_definition.rb | 1 + .../validation/data_type_validator.rb | 3 + .../validation/element_validator.rb | 3 +- .../cardinality_validator.rb | 34 +++++++ .../element_validator/data_type_validator.rb | 68 ++++++++++++++ .../fixed_value_validator.rb | 26 ++++++ .../validation/profile_validator.rb | 12 ++- .../validation/validation_result.rb | 1 + lib/fhir_models/validation/validator.rb | 1 - .../validation/value_set_validator.rb | 90 ++++++++++++++++++- .../fhir_models/bootstrap/my_example_spec.rb | 7 ++ .../cardinality_validator_spec.rb | 83 +++++++++++++++++ .../data_type_validator_spec.rb | 39 ++++++++ .../fixed_value_validator_spec.rb | 31 +++++++ 15 files changed, 396 insertions(+), 5 deletions(-) create mode 100644 lib/fhir_models/validation/data_type_validator.rb create mode 100644 lib/fhir_models/validation/element_validator/cardinality_validator.rb create mode 100644 lib/fhir_models/validation/element_validator/data_type_validator.rb create mode 100644 lib/fhir_models/validation/element_validator/fixed_value_validator.rb create mode 100644 spec/lib/fhir_models/bootstrap/my_example_spec.rb create mode 100644 spec/unit/validation/element_validator/cardinality_validator_spec.rb create mode 100644 spec/unit/validation/element_validator/data_type_validator_spec.rb create mode 100644 spec/unit/validation/element_validator/fixed_value_validator_spec.rb diff --git a/lib/fhir_models.rb b/lib/fhir_models.rb index d462e45aa..aa3a5d65a 100644 --- a/lib/fhir_models.rb +++ b/lib/fhir_models.rb @@ -35,6 +35,6 @@ end # Require validation code -Dir.glob(File.join(root, 'lib', 'fhir_models', 'validation', '*.rb')).each do |file| +Dir.glob(File.join(root, 'lib', 'fhir_models', 'validation', '**', '*.rb')).each do |file| require file end diff --git a/lib/fhir_models/fhir_ext/structure_definition.rb b/lib/fhir_models/fhir_ext/structure_definition.rb index fc8e51f4b..ef58d9416 100644 --- a/lib/fhir_models/fhir_ext/structure_definition.rb +++ b/lib/fhir_models/fhir_ext/structure_definition.rb @@ -297,6 +297,7 @@ def verify_cardinality(element, nodes) 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 + element.fixed end # data_type_code == a FHIR DataType code (see http://hl7.org/fhir/2015May/datatypes.html) diff --git a/lib/fhir_models/validation/data_type_validator.rb b/lib/fhir_models/validation/data_type_validator.rb new file mode 100644 index 000000000..91eb9b2f1 --- /dev/null +++ b/lib/fhir_models/validation/data_type_validator.rb @@ -0,0 +1,3 @@ +class DataTypeValidator + +end \ No newline at end of file diff --git a/lib/fhir_models/validation/element_validator.rb b/lib/fhir_models/validation/element_validator.rb index 6583bcbb5..ec2c8de9a 100644 --- a/lib/fhir_models/validation/element_validator.rb +++ b/lib/fhir_models/validation/element_validator.rb @@ -68,7 +68,8 @@ def self.verify_data_type(element, element_definition, current_path, skip = fals result = FHIR::ValidationResult.new result.element_definition = element_definition result.validation_type = :datatype - result.is_successful = :skipped + result.is_successful = :warn + result.text = "Unkown type: #{type_code}" result.element_path = current_path || element_definition.path return result end diff --git a/lib/fhir_models/validation/element_validator/cardinality_validator.rb b/lib/fhir_models/validation/element_validator/cardinality_validator.rb new file mode 100644 index 000000000..cc096cc6a --- /dev/null +++ b/lib/fhir_models/validation/element_validator/cardinality_validator.rb @@ -0,0 +1,34 @@ +module FHIR + module Validation + # Validator which allows cardinality validation of an element against an ElementDefinition + module CardinalityValidator + + # Verify that the element meets the cardinality requirements + # + # @param element [Object] The Element of the Resource under test + # @param element_definition [FHIR::ElementDefinition] The Element Definition from which the cardinality is taken + # @return result [FHIR::ValidationResult] The result of the cardinality check + def validate(element, element_definition, current_path = nil) + # Cardinality has no meaning on the first element. + # It is listed as optional(irrelevant) + # Specification Reference: http://www.hl7.org/fhir/elementdefinition.html#interpretation + # Zulip Chat: https://chat.fhir.org/#narrow/stream/179166-implementers/topic/cardinality.20of.20root.20elements/near/154024550 + return unless element_definition.path.include? '.' + + min = element_definition.min + max = element_definition.max == '*' ? Float::INFINITY : element_definition.max.to_i + result = FHIR::ValidationResult.new + # Save a reference to the ElementDefinition + result.element_definition = element_definition + result.validation_type = :cardinality + result.element_path = current_path || element_definition.path + result.element = element + element_array = [element].flatten.compact + result.is_successful = !((element_array.size < min) || (element_array.size > max)) + result + end + + module_function :validate + end + end +end diff --git a/lib/fhir_models/validation/element_validator/data_type_validator.rb b/lib/fhir_models/validation/element_validator/data_type_validator.rb new file mode 100644 index 000000000..c24a55caf --- /dev/null +++ b/lib/fhir_models/validation/element_validator/data_type_validator.rb @@ -0,0 +1,68 @@ +module FHIR + module Validation + # Validator which allows Data Type validation of an element against an ElementDefinition + module DataTypeValidator + + # Verify the individual FHIR Data Types + # + # FHIR Resources, Profiles and the StructureDefinitions are made up of FHIR Data Types. + # There are two kinds of structures that fall under the FHIR Data Types: complex-type and primitive-type. + # The snapshot of a resource does not contain the element definitions associated with primitive-type or complex-type + # structures it is composed of. + # + # https://www.hl7.org/fhir/datatypes.html + # @param element [Object] The Element of the Resource under test + # @param element_definition [FHIR::ElementDefinition] The Element Definition from which the cardinality is taken + # @return result [FHIR::ValidationResult] The result of the cardinality check + def validate(element, element_definition, current_path = nil) + return unless element_definition.path.include? '.' # Root Elements do not have a type + + # Can't do this validation if there is no type. + if element_definition.type.empty? + result = FHIR::ValidationResult.new + result.element_definition = element_definition + result.validation_type = :datatype + result.is_successful = :skipped + result.element_path = current_path || element_definition.path + return result + end + + # Get the type + type_code = if element_definition.type.one? + element_definition.type.first.code + else + return UnknownType.new('Need current_path in order to determine type') unless current_path + + element_definition.type.find do |datatype| + /[^.]+$/.match(element_definition.path.gsub('[x]', datatype.code.capitalize)) == /[^.]+$/.match(current_path) + end.code + end + type_def = FHIR::Definitions.type_definition(type_code) || FHIR::Definitions.resource_definition(type_code) + + # If we are missing the Structure Definition needed to do the validation. + if type_def.nil? + result = FHIR::ValidationResult.new + result.element_definition = element_definition + result.validation_type = :datatype + result.is_successful = :warn + result.text = "Unkown type: #{type_code}" + result.element_path = current_path || element_definition.path + return result + end + type_validator = FHIR::ProfileValidator.new(type_def) + results = type_validator.validate(element) + + results.each { |res| res.element_path = res.element_path.gsub(/^([^.]+)/, current_path) } + end + + module_function :validate + + # Error for Unknown Types + class UnknownType < StandardError + def initialize(msg = "Unknown TypeCode") + super(msg) + end + end + end + end +end diff --git a/lib/fhir_models/validation/element_validator/fixed_value_validator.rb b/lib/fhir_models/validation/element_validator/fixed_value_validator.rb new file mode 100644 index 000000000..5c1001f72 --- /dev/null +++ b/lib/fhir_models/validation/element_validator/fixed_value_validator.rb @@ -0,0 +1,26 @@ +module FHIR + module Validation + module FixedValueValidator + + # Validate the element matches the fixed value if a fixed value is provided + # + # @param element [Object] The Element of the Resource under test + # @param element_definition [FHIR::ElementDefinition] The Element Definition from which the cardinality is taken + # @return result [FHIR::ValidationResult] The result of the cardinality check + def validate(element, element_definition, current_path = nil) + fixed = element_definition.fixed + return unless fixed.respond_to?(:empty?) ? fixed.empty? : fixed.nil? + + result = FHIR::ValidationResult.new + result.element_definition = element_definition + result.validation_type = :fixed_value + result.element = element + + result.is_successful = (element == fixed) + result + end + + module_function :validate + end + end +end \ No newline at end of file diff --git a/lib/fhir_models/validation/profile_validator.rb b/lib/fhir_models/validation/profile_validator.rb index ffb6baffa..d21046d97 100644 --- a/lib/fhir_models/validation/profile_validator.rb +++ b/lib/fhir_models/validation/profile_validator.rb @@ -2,6 +2,7 @@ module FHIR # Validator which allows validation of a resource against a profile class ProfileValidator @vs_validators = {} + attr_accessor :all_results attr_accessor :show_skipped @@ -13,8 +14,13 @@ def initialize(profile) @profile = profile @all_results = [] @show_skipped = true + @element_validators = [] end + # Validate the provided resource + # + # @param resource [FHIR::Model] The Resource to be validated + # @return [Hash] the validation results def validate(resource) validate_against_hierarchy(resource) @all_results @@ -108,6 +114,10 @@ def validate(resource) hierarchy[:slices].values.each { |v| validate_hierarchy(resource, v, !element_exists) } end + def register_element_validator(element_validator) + @element_validators.push(element_validator) + end + private def blank?(obj) obj.respond_to?(:empty?) ? obj.empty? : obj.nil? end @@ -136,7 +146,7 @@ def validate(resource) results.push(ElementValidator.verify_element_cardinality(el, element_definition, p, skip)) # Don't validate an element that doesn't exist unless blank?(el) - # If there are multiple elements (like exte) + # If there are multiple elements (like extensions) if el.is_a? Array el.each_with_index do |v, k| results.push(*ElementValidator.verify_data_type(v, element_definition, "#{p}[#{k}]", skip)) diff --git a/lib/fhir_models/validation/validation_result.rb b/lib/fhir_models/validation/validation_result.rb index 61480a6c1..1a1bacde3 100644 --- a/lib/fhir_models/validation/validation_result.rb +++ b/lib/fhir_models/validation/validation_result.rb @@ -8,6 +8,7 @@ class ValidationResult attr_accessor :element_definition_id attr_accessor :element_path attr_accessor :profile + attr_accessor :text # Returns the validation result as an OperationOutcome # diff --git a/lib/fhir_models/validation/validator.rb b/lib/fhir_models/validation/validator.rb index 8beb03310..17fd750af 100644 --- a/lib/fhir_models/validation/validator.rb +++ b/lib/fhir_models/validation/validator.rb @@ -1,6 +1,5 @@ module FHIR # FHIR Resource Validator - # Implementation inspired by FHIR HAPI Validation class Validator attr_reader :validator_modules attr_reader :show_skipped diff --git a/lib/fhir_models/validation/value_set_validator.rb b/lib/fhir_models/validation/value_set_validator.rb index 5ae4bc8d0..c7ce01eab 100644 --- a/lib/fhir_models/validation/value_set_validator.rb +++ b/lib/fhir_models/validation/value_set_validator.rb @@ -1,2 +1,90 @@ -class ValueSetValidator +# Provides terminology validation +class TerminologyValidator + attr_accessor :vs_validators + + def initialize + @vs_validators = {} + end + + def clear_validator(valueset_uri) + @vs_validators.delete valueset_uri + end + + def clear_all_validators() + @vs_validators = {} + end + + def add_validator(valueset_uri, &validator_fn) + @vs_validators[valueset_uri] = validator_fn + end + + # A Coding contains a system and a code + # + # The code and system will be checked against the code system itself if a valueset is not provided + def validate_coding(code, system, valueset = nil) + valueset ||= system + @vs_validators[valueset].call('system' => system, 'code' => code) + end + + def validate_code(code, valueset) + valueset ||= system + @vs_validators[valueset].call(code) + end + + def terminology_result + result = FHIR::ValidationResult.new + end + + def validate(element, element_definition, current_path, skip = false) + results = [] + binding = element_definition.binding + # TODO: Could also not return here and return a skip for elements without a binding + #return if binding.nil? + + valueSet = binding&.valueSet + + type_code = if element_definition.type.one? + element_definition.type.first.code + else + element_definition.type.find do |datatype| + /[^.]+$/.match(element_definition.path.gsub('[x]', datatype.code.capitalize)) == /[^.]+$/.match(current_path) + end&.code + end + + if type_code == 'CodeableConcept' + element.coding.each_with_index do |coding, index| + result = FHIR::ValidationResult.new + result.element_definition = element_definition + result.element_path = "#{current_path}[#{index}]" + result.validation_type = :terminology + if coding.code.nil? || coding.system.nil? + result.is_successful = :skipped + result.text = "#{current_path}[#{index}] has no #{'code' if coding.code.nil?}" \ + "#{' or' if coding.code.nil? && coding.system.nil?} #{' system' if coding.system.nil?} ." + else + result.is_successful = validate_coding(coding.code, coding.system) + end + results.push result + end + elsif ['Coding', 'Quantity'].include? type_code + result = FHIR::ValidationResult.new + result.element_definition = element_definition + result.element_path = current_path + result.validation_type = :terminology + if element.code.nil? || element.system.nil? + result.is_successful = :skipped + result.text = "#{current_path} has no #{'code' if coding.code.nil?}" \ + "#{' or' if coding.code.nil? && coding.system.nil?} #{' system' if coding.system.nil?} ." + else + result.is_successful = validate_coding(element.code, element.system) + end + results.push result + elsif type_code == 'code' + result = FHIR::ValidationResult.new + result.element_definition = element_definition + result.element_path = current_path + result.validation_type = :terminology + result.is_successful = :skipped + end + end end diff --git a/spec/lib/fhir_models/bootstrap/my_example_spec.rb b/spec/lib/fhir_models/bootstrap/my_example_spec.rb new file mode 100644 index 000000000..33f9ab813 --- /dev/null +++ b/spec/lib/fhir_models/bootstrap/my_example_spec.rb @@ -0,0 +1,7 @@ +describe 'FHIR::JSON' do + + it 'allows FHIR::Models to be represented as JSON' do + patient = FHIR::Patient.new + expect(patient.to_json).to eq("{\n \"resourceType\": \"Patient\"\n}") + end +end \ No newline at end of file diff --git a/spec/unit/validation/element_validator/cardinality_validator_spec.rb b/spec/unit/validation/element_validator/cardinality_validator_spec.rb new file mode 100644 index 000000000..52572246d --- /dev/null +++ b/spec/unit/validation/element_validator/cardinality_validator_spec.rb @@ -0,0 +1,83 @@ +describe FHIR::Validation::DataTypeValidator do + + let(:validator) { FHIR::Validation::DataTypeValidator } + + let(:element) { double({'name' => 'Bob'}) } + let(:element_definition) { instance_double(FHIR::ElementDefinition) } + + describe '#validate' do + it 'returns a single validation results related to cardinality' do + + allow(element_definition).to receive(:min) + .and_return(0) + allow(element_definition).to receive(:max) + .and_return(1) + allow(element_definition).to receive(:path) + .and_return('Foo.bar') + + results = validator.validate(element, element_definition) + expect(results.validation_type).to be(:cardinality) + expect(results.is_successful).to be(true) + end + + it 'detects when too many elements are present' do + allow(element_definition).to receive(:min) + .and_return(0) + allow(element_definition).to receive(:max) + .and_return(1) + allow(element_definition).to receive(:path) + .and_return('Foo.bar') + + results = validator.validate([element, element], element_definition) + expect(results.validation_type).to be(:cardinality) + expect(results.is_successful).to be(false) + end + + it 'detects when too few elements are present' do + allow(element_definition).to receive(:min) + .and_return(1) + allow(element_definition).to receive(:max) + .and_return('*') + allow(element_definition).to receive(:path) + .and_return('Foo.bar') + + results = validator.validate(nil, element_definition) + expect(results.validation_type).to be(:cardinality) + expect(results.is_successful).to be(false) + + results = validator.validate([], element_definition) + expect(results.validation_type).to be(:cardinality) + expect(results.is_successful).to be(false) + end + + it 'can check for an exact number of elements to be present' do + allow(element_definition).to receive(:min) + .and_return(2) + allow(element_definition).to receive(:max) + .and_return(2) + allow(element_definition).to receive(:path) + .and_return('Foo.bar') + + results = validator.validate([element, element], element_definition) + expect(results.validation_type).to be(:cardinality) + expect(results.is_successful).to be(true) + + results = validator.validate(element, element_definition) + expect(results.validation_type).to be(:cardinality) + expect(results.is_successful).to be(false) + end + + it 'skips cardinality on the root element' do + allow(element_definition).to receive(:min) + .and_return(0) + allow(element_definition).to receive(:max) + .and_return('*') + allow(element_definition).to receive(:path) + .and_return('Foo') + + results = validator.validate(element, element_definition) + expect(results.validation_type).to be(:cardinality) + expect(results.is_successful).to be(:skipped) + end + end +end \ No newline at end of file diff --git a/spec/unit/validation/element_validator/data_type_validator_spec.rb b/spec/unit/validation/element_validator/data_type_validator_spec.rb new file mode 100644 index 000000000..11c914c01 --- /dev/null +++ b/spec/unit/validation/element_validator/data_type_validator_spec.rb @@ -0,0 +1,39 @@ +describe FHIR::Validation::DataTypeValidator do + + let(:validator) { FHIR::Validation::DataTypeValidator } + + let(:element) { double(true) } + let(:element_definition) { instance_double(FHIR::ElementDefinition) } + let(:type1) do + type1 = FHIR::ElementDefinition::Type.new + type1.code = 'boolean' + type1 + end + let(:type2) do + type2 = FHIR::ElementDefinition::Type.new + type2.code = 'dateTime' + type2 + end + let(:types) do + [type1, type2] + end + + describe '#validate' do + it 'returns an array of results for elements with a choice of type' do + allow(element_definition).to receive(:path) + .and_return('Foo.bar[x]') + allow(element_definition).to receive(:type) + .and_return(types) + results = validator.validate(element, element_definition, 'Foo.barBoolean') + expect(results).to include( an_object_having_attributes(validation_type: :cardinality)) + end + it 'returns an array of results for elements with a single type' do + allow(element_definition).to receive(:path) + .and_return('Foo.bar') + allow(element_definition).to receive(:type) + .and_return([type1]) + results = validator.validate(element, element_definition, 'Foo.bar') + expect(results).to include( an_object_having_attributes(validation_type: :cardinality)) + end + end +end \ No newline at end of file diff --git a/spec/unit/validation/element_validator/fixed_value_validator_spec.rb b/spec/unit/validation/element_validator/fixed_value_validator_spec.rb new file mode 100644 index 000000000..1ee6ac880 --- /dev/null +++ b/spec/unit/validation/element_validator/fixed_value_validator_spec.rb @@ -0,0 +1,31 @@ +describe FHIR::Validation::FixedValueValidator do + + let(:validator) { FHIR::Validation::FixedValueValidator } + + let(:element) { double('867-5309') } + #let(:element_definition) { instance_double(FHIR::ElementDefinition) } + let(:element_definition) { FHIR::ElementDefinition.new } + + describe '#validate' do + it 'returns a single result related to the fixed value' do + + # allow(element_definition).to receive(:fixed) + # .and_return(element) + element_definition.fixedString = element + + results = validator.validate(element, element_definition) + expect(results.validation_type).to be(:fixed_value) + expect(results.is_successful).to be(true) + end + + it 'detects when the fixed value is incorrect' do + + # allow(element_definition).to receive(:fixed) + # .and_return('555-5555') + + results = validator.validate(element, element_definition) + expect(results.validation_type).to be(:fixed_value) + expect(results.is_successful).to be(false) + end + end +end \ No newline at end of file From 9d104fd35b7d3cc368a8a086dbb3eaaf0714cca4 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Thu, 25 Jul 2019 11:10:21 -0400 Subject: [PATCH 09/80] use registered element_validators --- .../validation/profile_validator.rb | 28 ++++--------------- spec/unit/validation/validation_api_spec.rb | 18 ++++++++++++ 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/lib/fhir_models/validation/profile_validator.rb b/lib/fhir_models/validation/profile_validator.rb index d21046d97..9d773f2bb 100644 --- a/lib/fhir_models/validation/profile_validator.rb +++ b/lib/fhir_models/validation/profile_validator.rb @@ -5,6 +5,7 @@ class ProfileValidator attr_accessor :all_results attr_accessor :show_skipped + attr_accessor :element_validators # Create a ProfileValidator from a FHIR::StructureDefinition # @@ -127,36 +128,19 @@ def register_element_validator(element_validator) # @param resource [FHIR::Model] The resource to be validated # @param element_definition [FHIR::ElementDefinition] The ElementDefintion Resource which provides the validation criteria private def verify_elements(elements, element_definition, skip = false) - # This will hold the FHIR::ValidationResults from the various checks - results = [] - # Normalize type choice elements to just the element for cardinality testing if element_definition.path.end_with? '[x]' - mtelms = Hash.new([]) + choice_type_elements = Hash.new([]) elements.each do |k, v| renorm = k.rpartition('.').first - mtelms["#{renorm}.#{element_definition.path.split('.').last}"].push(v) - end - mtelms.each do |p, el| - results.push(ElementValidator.verify_element_cardinality(el, element_definition, p, skip)) + choice_type_elements["#{renorm}.#{element_definition.path.split('.').last}"].push(v) end + elements = choice_type_elements end - elements.each do |p, el| - results.push(ElementValidator.verify_element_cardinality(el, element_definition, p, skip)) - # Don't validate an element that doesn't exist - unless blank?(el) - # If there are multiple elements (like extensions) - if el.is_a? Array - el.each_with_index do |v, k| - results.push(*ElementValidator.verify_data_type(v, element_definition, "#{p}[#{k}]", skip)) - end - else - results.push(*ElementValidator.verify_data_type(el, element_definition, p, skip)) - end - end + elements.flat_map do |path, el| + @element_validators.map { |validator| validator.validate(el, element_definition, path)} end - results end # Splits a path into an array diff --git a/spec/unit/validation/validation_api_spec.rb b/spec/unit/validation/validation_api_spec.rb index c7c937563..287f6f716 100644 --- a/spec/unit/validation/validation_api_spec.rb +++ b/spec/unit/validation/validation_api_spec.rb @@ -2,11 +2,29 @@ FIXTURES_DIR = File.join('test', 'fixtures') let(:validator) {FHIR::Validator.new} + let(:profile_validator) { FHIR::ProfileValidator.new(structure_definition)} + let(:cardinality_validator) { spy(FHIR::Validation::CardinalityValidator)} + let(:element_definition) do + element_definition = FHIR::ElementDefinition.new(id: 'Patient', path: 'Patient') + end + let(:structure_definition) do + FHIR::StructureDefinition.new(snapshot:{element: [element_definition]}) + end + let(:resource) do + {} + end it 'initially has no validator modules' do expect(validator.validator_modules).to be_empty end + specify '#register_element_validator' do + profile_validator.register_element_validator(cardinality_validator) + profile_validator.validate(resource) + expect(cardinality_validator).to have_received(:validate) + .with(resource, element_definition, element_definition.path) + end + context 'with US Core Patient Profile Validator' do let(:us_core_profile_validator) do us_core_patient = File.join(FIXTURES_DIR, 'us_core', 'StructureDefinition-us-core-patient.json') From 8b16dd926a635f17380a668bbfb6fa28651f8949 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Thu, 25 Jul 2019 15:34:08 -0400 Subject: [PATCH 10/80] break out element retrieval --- .../fhir_ext/element_definition.rb | 4 + .../cardinality_validator.rb | 4 + .../validation/profile_validator.rb | 106 ---------------- lib/fhir_models/validation/retrieval.rb | 117 ++++++++++++++++++ spec/unit/validation/retrieval_spec.rb | 67 ++++++++++ 5 files changed, 192 insertions(+), 106 deletions(-) create mode 100644 lib/fhir_models/validation/retrieval.rb create mode 100644 spec/unit/validation/retrieval_spec.rb diff --git a/lib/fhir_models/fhir_ext/element_definition.rb b/lib/fhir_models/fhir_ext/element_definition.rb index 5277e1a3c..59ec3fec5 100644 --- a/lib/fhir_models/fhir_ext/element_definition.rb +++ b/lib/fhir_models/fhir_ext/element_definition.rb @@ -45,5 +45,9 @@ def print_children(spaces = 0) end nil end + + def choice_type? + path.end_with? '[x]' + end end end diff --git a/lib/fhir_models/validation/element_validator/cardinality_validator.rb b/lib/fhir_models/validation/element_validator/cardinality_validator.rb index cc096cc6a..e118cec7c 100644 --- a/lib/fhir_models/validation/element_validator/cardinality_validator.rb +++ b/lib/fhir_models/validation/element_validator/cardinality_validator.rb @@ -2,6 +2,7 @@ module FHIR module Validation # Validator which allows cardinality validation of an element against an ElementDefinition module CardinalityValidator + include FHIR::Validation::Retrieval # Verify that the element meets the cardinality requirements # @@ -15,6 +16,9 @@ def validate(element, element_definition, current_path = nil) # Zulip Chat: https://chat.fhir.org/#narrow/stream/179166-implementers/topic/cardinality.20of.20root.20elements/near/154024550 return unless element_definition.path.include? '.' + elements = retrieve_element_by_element_defintion(resource, element_definition, normalized: true) + + min = element_definition.min max = element_definition.max == '*' ? Float::INFINITY : element_definition.max.to_i result = FHIR::ValidationResult.new diff --git a/lib/fhir_models/validation/profile_validator.rb b/lib/fhir_models/validation/profile_validator.rb index 9d773f2bb..2d75520f5 100644 --- a/lib/fhir_models/validation/profile_validator.rb +++ b/lib/fhir_models/validation/profile_validator.rb @@ -128,16 +128,6 @@ def register_element_validator(element_validator) # @param resource [FHIR::Model] The resource to be validated # @param element_definition [FHIR::ElementDefinition] The ElementDefintion Resource which provides the validation criteria private def verify_elements(elements, element_definition, skip = false) - # Normalize type choice elements to just the element for cardinality testing - if element_definition.path.end_with? '[x]' - choice_type_elements = Hash.new([]) - elements.each do |k, v| - renorm = k.rpartition('.').first - choice_type_elements["#{renorm}.#{element_definition.path.split('.').last}"].push(v) - end - elements = choice_type_elements - end - elements.flat_map do |path, el| @element_validators.map { |validator| validator.validate(el, element_definition, path)} end @@ -148,102 +138,6 @@ def register_element_validator(element_validator) element_definition.path.split('.') end - # Retrieves all the elements associated with the path - # i.e. Patient.contact.name will return an array with all the names. - # - # @param path [String] the path - # @param resource [FHIR::Model] the resource from which the elements will be retrieved - # @return [Array] The desired elements - private def retrieve_element(path, resource) - path.split('.').drop(1).inject(resource) do |memo, meth| - [memo].flatten.map { |thing| thing.send(meth) } - end - end - - # Retrieves all the elements associated with the given path with the FHIRPath of the element - # - # - # @param path [String] the path - # @param resource [FHIR::Model] the resource from which the elements will be retrieved - # @return [Hash] The desired elements - private def retrieve_element_with_fhirpath(path, resource, indexed = true) - spath = path.split('.') - base = spath.shift - fhirpath_elements = { base => resource } - last = spath.pop unless indexed - - desired_elements = spath.inject(fhirpath_elements) do |memo, meth| - digging = {} - memo.each do |k, v| - elms = v.send(meth) - # More than one element where the FHIRPath needs indexing - if elms.respond_to? :each_with_index - elms.each_with_index do |vv, kk| - digging["#{k}.#{meth}[#{kk}]"] = vv unless blank?(vv) - end - # Just One - else - digging["#{k}.#{meth}"] = elms unless blank?(elms) - end - end - digging - end - - return desired_elements unless last - - # If we don't want to index the last element (useful for testing cardinality) - not_indexed = {} - desired_elements.each do |k, v| - elms = v.send(last) if v.is_a? FHIR::Model # FHIR Primitives are not modeled and will throw NoMethod Error - not_indexed["#{k}.#{last}"] = elms - end - not_indexed - end - - private def retrieve_by_element_definition(elementdefinition, resource, indexed = true) - # Check if we were provided a path that includes extensions (like in the ElementDefinition id versus the path) - path = elementdefinition.path - - # Check for multiple choice types - if path.include? '[x]' - elements = {} - elementdefinition.type.each do |type| - choice_type = type.code[0].upcase + type.code[1..-1] - type_element = retrieve_element_with_fhirpath(path.gsub('[x]', choice_type), resource, indexed) - elements.merge!(type_element) unless blank?(type_element) - end - else - elements = retrieve_element_with_fhirpath(path, resource, indexed) - end - - # Handle Slices - if elementdefinition.sliceName - # Grab Extension slices - raise UnhandledSlice("Slice has more than one type. #{elementdefinition.id}") unless elementdefinition.type.one? - - raise UnhandledSlice("Slice type #{elementdefinition.type.code} is not handled. Only Extension slices are handled") unless elementdefinition.type.first.code == 'Extension' - - # Only select the elements which match the slice profile. - if indexed - # Elements are already indexed - elements.select! do |_k, v| - v.url == elementdefinition.type.first.profile.first - end - else - indexed_elements = {} - elements.each do |k, v| - v.each_with_index do |vv, kk| - if vv.url == elementdefinition.type.first.profile.first - indexed_elements["#{k}[#{kk}]"] = vv - end - end - end - elements = indexed_elements - end - end - elements - end - # Method for registering ValueSet and CodeSystem Validators # # @param valueset_uri [String] The ValueSet URI the validator is related to diff --git a/lib/fhir_models/validation/retrieval.rb b/lib/fhir_models/validation/retrieval.rb new file mode 100644 index 000000000..000ea8809 --- /dev/null +++ b/lib/fhir_models/validation/retrieval.rb @@ -0,0 +1,117 @@ +module FHIR + module Validation + # Methods for retrieving elements from resources + module Retrieval + + def blank?(obj) + obj.respond_to?(:empty?) ? obj.empty? : obj.nil? + end + + # Retrieves all the elements associated with the path + # i.e. Patient.contact.name will return an array with all the names. + # + # @param path [String] the path + # @param resource [FHIR::Model] the resource from which the elements will be retrieved + # @return [Array] The desired elements + def retrieve_element(path, resource) + path.split('.').drop(1).inject(resource) do |memo, meth| + [memo].flatten.map { |thing| thing.send(meth) } + end + end + + # Retrieves all the elements associated with the given path with the FHIRPath of the element + # + # @param path [String] the path + # @param resource [FHIR::Model] the resource from which the elements will be retrieved + # @return [Hash] The desired elements + def retrieve_element_with_fhirpath(path, resource, indexed = true) + spath = path.split('.') + base = spath.shift + fhirpath_elements = { base => resource } + last = spath.pop unless indexed + + desired_elements = spath.inject(fhirpath_elements) do |memo, meth| + digging = {} + memo.each do |k, v| + elms = v.send(meth) + # More than one element where the FHIRPath needs indexing + if elms.respond_to? :each_with_index + elms.each_with_index do |vv, kk| + digging["#{k}.#{meth}[#{kk}]"] = vv unless blank?(vv) + end + # Just One + else + digging["#{k}.#{meth}"] = elms unless blank?(elms) + end + end + digging + end + + return desired_elements unless last + + # If we don't want to index the last element (useful for testing cardinality) + not_indexed = {} + desired_elements.each do |k, v| + elms = v.send(last) if v.is_a? FHIR::Model # FHIR Primitives are not modeled and will throw NoMethod Error + not_indexed["#{k}.#{last}"] = elms + end + not_indexed + end + + def retrieve_by_element_definition(resource, element_definition, indexed: false, normalized: false) + # Check if we were provided a path that includes extensions (like in the ElementDefinition id versus the path) + path = element_definition.path + + # Check for multiple choice types + if element_definition.choice_type? + elements = {} + element_definition.type.each do |type| + choice_type = type.code[0].upcase + type.code[1..-1] + type_element = retrieve_element_with_fhirpath(path.gsub('[x]', choice_type), resource, indexed) + elements.merge!(type_element) unless blank?(type_element) + end + if normalized + choice_type_elements = Hash.new([]) + elements.each do |k, v| + renorm = k.rpartition('.').first + normalized_path = "#{renorm}.#{element_definition.path.split('.').last}" + choice_type_elements[normalized_path] = choice_type_elements[normalized_path].push(v) + end + elements = choice_type_elements + end + else + elements = retrieve_element_with_fhirpath(path, resource, indexed) + end + + # Handle Slices + if element_definition.sliceName + # Grab Extension slices + raise UnhandledSlice("Slice has more than one type. #{element_definition.id}") unless element_definition.type.one? + + raise UnhandledSlice("Slice type #{element_definition.type.code} is not handled. Only Extension slices are handled") unless element_definition.type.first.code == 'Extension' + + # Only select the elements which match the slice profile. + if indexed + # Elements are already indexed + elements.select! do |_k, v| + v.url == element_definition.type.first.profile.first + end + else + indexed_elements = {} + elements.each do |k, v| + v.each_with_index do |vv, kk| + if vv.url == element_definition.type.first.profile.first + indexed_elements["#{k}[#{kk}]"] = vv + end + end + end + elements = indexed_elements + end + end + elements + end + + module_function :retrieve_by_element_definition, :retrieve_element_with_fhirpath, :blank? + end + end +end \ No newline at end of file diff --git a/spec/unit/validation/retrieval_spec.rb b/spec/unit/validation/retrieval_spec.rb new file mode 100644 index 000000000..b37d224ba --- /dev/null +++ b/spec/unit/validation/retrieval_spec.rb @@ -0,0 +1,67 @@ +describe FHIR::Validation::Retrieval do + let(:retrieval) { FHIR::Validation::Retrieval } + let(:resource) do + FHIR::Patient.new(id: 2, + name: {given: 'Bob'}, + communication: [{language: 'English'}, {language: 'Spanish'}], + deceasedBoolean: false, + deceasedDateTime: 'YYYY-MM-DD') + end + + describe '#retrieve_by_element_definition' do + it 'returns the root element or a resource' do + element_definition = FHIR::ElementDefinition.new(id: 'Patient', path: 'Patient') + expect(retrieval.retrieve_by_element_definition(resource, element_definition)).to eq({element_definition.id => resource}) + end + + it 'returns an attribute of element' do + element_definition = FHIR::ElementDefinition.new(id: 'Patient.id', path: 'Patient.id') + expect(retrieval.retrieve_by_element_definition(resource, element_definition)).to eq({element_definition.id => resource.id}) + end + + context 'with elements that have a cardinality greater than one' do + let(:element_definition) {FHIR::ElementDefinition.new(id: 'Patient.communication', path: 'Patient.communication')} + it 'returns attributes as an array' do + expect(retrieval.retrieve_by_element_definition(resource, element_definition)).to eq({element_definition.id => resource.communication}) + end + + it 'returns elements indexed' do + expected_communications = { + "#{element_definition.id}[0]" => resource.communication.first, + "#{element_definition.id}[1]" => resource.communication.last + } + expect(retrieval.retrieve_by_element_definition(resource, element_definition, indexed: true)).to eq(expected_communications) + end + end + + context 'with choice of types' do + let(:element_definition) do + FHIR::ElementDefinition.new(id: 'Patient.deceased[x]', + path: 'Patient.deceased[x]', + type: [{code: 'boolean'}, {code: 'dateTime'}]) + end + it 'returns all the elements present if there is a choice of type' do + titlecase_choice = [ + element_definition.id.gsub('[x]', + element_definition.type.first.code.capitalize), + element_definition.id.gsub('[x]', + "#{element_definition.type.last.code[0].capitalize}"\ + "#{element_definition.type.last.code[1..-1]}")] + expected_communications = { + titlecase_choice.first => resource.deceasedBoolean, + titlecase_choice.last => resource.deceasedDateTime + } + expect(retrieval.retrieve_by_element_definition(resource, element_definition)).to eq(expected_communications) + end + + it 'returns an array of all the elements present if there is a choice of type normalized to the element' do + expected_communications = { element_definition.id => [ resource.deceasedBoolean, resource.deceasedDateTime ]} + expect(retrieval.retrieve_by_element_definition(resource, element_definition, normalized: true)).to eq(expected_communications) + end + end + end + + specify '#retrieve_element_with_fhirpath' do + + end +end \ No newline at end of file From 9980542f5123a845c28be22760defbc412187156 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Thu, 25 Jul 2019 18:12:53 -0400 Subject: [PATCH 11/80] update cardinality check --- .../cardinality_validator.rb | 25 +-- .../element_validator/data_type_validator.rb | 25 ++- .../fixed_value_validator.rb | 4 +- .../validation/profile_validator.rb | 22 +-- lib/fhir_models/validation/retrieval.rb | 24 +-- .../cardinality_validator_spec.rb | 174 +++++++++++------- .../data_type_validator_spec.rb | 61 +++--- 7 files changed, 188 insertions(+), 147 deletions(-) diff --git a/lib/fhir_models/validation/element_validator/cardinality_validator.rb b/lib/fhir_models/validation/element_validator/cardinality_validator.rb index e118cec7c..c0a820600 100644 --- a/lib/fhir_models/validation/element_validator/cardinality_validator.rb +++ b/lib/fhir_models/validation/element_validator/cardinality_validator.rb @@ -2,37 +2,40 @@ module FHIR module Validation # Validator which allows cardinality validation of an element against an ElementDefinition module CardinalityValidator - include FHIR::Validation::Retrieval - # Verify that the element meets the cardinality requirements # # @param element [Object] The Element of the Resource under test # @param element_definition [FHIR::ElementDefinition] The Element Definition from which the cardinality is taken # @return result [FHIR::ValidationResult] The result of the cardinality check - def validate(element, element_definition, current_path = nil) + def self.validate(resource, element_definition) # Cardinality has no meaning on the first element. # It is listed as optional(irrelevant) # Specification Reference: http://www.hl7.org/fhir/elementdefinition.html#interpretation # Zulip Chat: https://chat.fhir.org/#narrow/stream/179166-implementers/topic/cardinality.20of.20root.20elements/near/154024550 return unless element_definition.path.include? '.' - elements = retrieve_element_by_element_defintion(resource, element_definition, normalized: true) - + elements = FHIR::Validation::Retrieval.retrieve_by_element_definition(resource, + element_definition, + normalized: true) + elements.flat_map do |path, el| + el = [el].flatten.compact + validate_element(el, element_definition, path) + end + end + def self.validate_element(element_collection, element_definition, path) min = element_definition.min max = element_definition.max == '*' ? Float::INFINITY : element_definition.max.to_i result = FHIR::ValidationResult.new - # Save a reference to the ElementDefinition result.element_definition = element_definition result.validation_type = :cardinality - result.element_path = current_path || element_definition.path - result.element = element - element_array = [element].flatten.compact - result.is_successful = !((element_array.size < min) || (element_array.size > max)) + result.element_path = path + result.element = element_collection + result.is_successful = !((element_collection.length < min) || (element_collection.length > max)) result end - module_function :validate + private_class_method :validate_element end end end diff --git a/lib/fhir_models/validation/element_validator/data_type_validator.rb b/lib/fhir_models/validation/element_validator/data_type_validator.rb index c24a55caf..97d2e4c95 100644 --- a/lib/fhir_models/validation/element_validator/data_type_validator.rb +++ b/lib/fhir_models/validation/element_validator/data_type_validator.rb @@ -14,16 +14,26 @@ module DataTypeValidator # @param element [Object] The Element of the Resource under test # @param element_definition [FHIR::ElementDefinition] The Element Definition from which the cardinality is taken # @return result [FHIR::ValidationResult] The result of the cardinality check - def validate(element, element_definition, current_path = nil) + def self.validate(resource, element_definition) return unless element_definition.path.include? '.' # Root Elements do not have a type + elements = FHIR::Validation::Retrieval.retrieve_by_element_definition(resource, + element_definition, + indexed: true) + + elements.flat_map do |path, el| + validate_element(el, element_definition, path) + end + end + + def self.validate_element(element, element_definition, path) # Can't do this validation if there is no type. if element_definition.type.empty? result = FHIR::ValidationResult.new result.element_definition = element_definition result.validation_type = :datatype result.is_successful = :skipped - result.element_path = current_path || element_definition.path + result.element_path = path || element_definition.path return result end @@ -31,10 +41,10 @@ def validate(element, element_definition, current_path = nil) type_code = if element_definition.type.one? element_definition.type.first.code else - return UnknownType.new('Need current_path in order to determine type') unless current_path + return UnknownType.new('Need path in order to determine type') unless path element_definition.type.find do |datatype| - /[^.]+$/.match(element_definition.path.gsub('[x]', datatype.code.capitalize)) == /[^.]+$/.match(current_path) + /[^.]+$/.match(element_definition.path.gsub('[x]', datatype.code.capitalize)) == /[^.]+$/.match(path) end.code end type_def = FHIR::Definitions.type_definition(type_code) || FHIR::Definitions.resource_definition(type_code) @@ -46,17 +56,16 @@ def validate(element, element_definition, current_path = nil) result.validation_type = :datatype result.is_successful = :warn result.text = "Unkown type: #{type_code}" - result.element_path = current_path || element_definition.path + result.element_path = path || element_definition.path return result end type_validator = FHIR::ProfileValidator.new(type_def) + type_validator.register_element_validator(FHIR::Validation::CardinalityValidator) results = type_validator.validate(element) - results.each { |res| res.element_path = res.element_path.gsub(/^([^.]+)/, current_path) } + results.each { |res| res.element_path = res.element_path.gsub(/^([^.]+)/, path) } end - module_function :validate - # Error for Unknown Types class UnknownType < StandardError def initialize(msg = "Unknown TypeCode") diff --git a/lib/fhir_models/validation/element_validator/fixed_value_validator.rb b/lib/fhir_models/validation/element_validator/fixed_value_validator.rb index 5c1001f72..e502dbd04 100644 --- a/lib/fhir_models/validation/element_validator/fixed_value_validator.rb +++ b/lib/fhir_models/validation/element_validator/fixed_value_validator.rb @@ -7,10 +7,12 @@ module FixedValueValidator # @param element [Object] The Element of the Resource under test # @param element_definition [FHIR::ElementDefinition] The Element Definition from which the cardinality is taken # @return result [FHIR::ValidationResult] The result of the cardinality check - def validate(element, element_definition, current_path = nil) + def validate(resource, element_definition, current_path = nil) fixed = element_definition.fixed return unless fixed.respond_to?(:empty?) ? fixed.empty? : fixed.nil? + elements = retrieve_by_element_definition(resource, element_definition, indexed: true) + result = FHIR::ValidationResult.new result.element_definition = element_definition result.validation_type = :fixed_value diff --git a/lib/fhir_models/validation/profile_validator.rb b/lib/fhir_models/validation/profile_validator.rb index 2d75520f5..f59d94c37 100644 --- a/lib/fhir_models/validation/profile_validator.rb +++ b/lib/fhir_models/validation/profile_validator.rb @@ -90,17 +90,13 @@ def validate(resource) hierarchy[:results] ||= [] element_definition = hierarchy[:elementDefinition] - elements = retrieve_by_element_definition(element_definition, resource, false) - result = [] - # Get the Results - profile_results = verify_elements(elements, element_definition, skip) - profile_results.each { |res| res.profile ||= @profile.url } - result.push(*profile_results) + results = @element_validators.flat_map { |validator| validator.validate(resource, element_definition, path)} + results.each { |res| res.profile ||= @profile.url } # Save the validation results - hierarchy[:results].push(*result) - @all_results.push(*result) + hierarchy[:results].push(*results) + @all_results.push(*results) # Check to see if there are any valid elements to determine if we need to check the subelements element_exists = !blank?(elements.values.flatten.compact) @@ -123,16 +119,6 @@ def register_element_validator(element_validator) obj.respond_to?(:empty?) ? obj.empty? : obj.nil? end - # Verify the element in the provided resource based on the provided ElementDefinition - # - # @param resource [FHIR::Model] The resource to be validated - # @param element_definition [FHIR::ElementDefinition] The ElementDefintion Resource which provides the validation criteria - private def verify_elements(elements, element_definition, skip = false) - elements.flat_map do |path, el| - @element_validators.map { |validator| validator.validate(el, element_definition, path)} - end - end - # Splits a path into an array private def element_path_array(element_definition) element_definition.path.split('.') diff --git a/lib/fhir_models/validation/retrieval.rb b/lib/fhir_models/validation/retrieval.rb index 000ea8809..9859ffc04 100644 --- a/lib/fhir_models/validation/retrieval.rb +++ b/lib/fhir_models/validation/retrieval.rb @@ -2,23 +2,6 @@ module FHIR module Validation # Methods for retrieving elements from resources module Retrieval - - def blank?(obj) - obj.respond_to?(:empty?) ? obj.empty? : obj.nil? - end - - # Retrieves all the elements associated with the path - # i.e. Patient.contact.name will return an array with all the names. - # - # @param path [String] the path - # @param resource [FHIR::Model] the resource from which the elements will be retrieved - # @return [Array] The desired elements - def retrieve_element(path, resource) - path.split('.').drop(1).inject(resource) do |memo, meth| - [memo].flatten.map { |thing| thing.send(meth) } - end - end - # Retrieves all the elements associated with the given path with the FHIRPath of the element # # @param path [String] the path @@ -111,7 +94,12 @@ def retrieve_by_element_definition(resource, element_definition, indexed: false, elements end - module_function :retrieve_by_element_definition, :retrieve_element_with_fhirpath, :blank? + def self.blank?(obj) + obj.respond_to?(:empty?) ? obj.empty? : obj.nil? + end + + private_class_method :blank? + module_function :retrieve_by_element_definition, :retrieve_element_with_fhirpath end end end \ No newline at end of file diff --git a/spec/unit/validation/element_validator/cardinality_validator_spec.rb b/spec/unit/validation/element_validator/cardinality_validator_spec.rb index 52572246d..60497af73 100644 --- a/spec/unit/validation/element_validator/cardinality_validator_spec.rb +++ b/spec/unit/validation/element_validator/cardinality_validator_spec.rb @@ -1,83 +1,119 @@ -describe FHIR::Validation::DataTypeValidator do - - let(:validator) { FHIR::Validation::DataTypeValidator } - - let(:element) { double({'name' => 'Bob'}) } - let(:element_definition) { instance_double(FHIR::ElementDefinition) } +describe FHIR::Validation::CardinalityValidator do + let(:validator) { FHIR::Validation::CardinalityValidator } + let(:resource) do + FHIR::Patient.new(id: 2) + end describe '#validate' do - it 'returns a single validation results related to cardinality' do - - allow(element_definition).to receive(:min) - .and_return(0) - allow(element_definition).to receive(:max) - .and_return(1) - allow(element_definition).to receive(:path) - .and_return('Foo.bar') - - results = validator.validate(element, element_definition) - expect(results.validation_type).to be(:cardinality) - expect(results.is_successful).to be(true) + let(:element_definition) do FHIR::ElementDefinition.new(min: 0, + max: 1, + id: 'Patient', + path: 'Patient') end - it 'detects when too many elements are present' do - allow(element_definition).to receive(:min) - .and_return(0) - allow(element_definition).to receive(:max) - .and_return(1) - allow(element_definition).to receive(:path) - .and_return('Foo.bar') - - results = validator.validate([element, element], element_definition) - expect(results.validation_type).to be(:cardinality) - expect(results.is_successful).to be(false) + it 'skips cardinality on the root element' do + results = validator.validate(resource, element_definition) + expect(results).to be_nil end - it 'detects when too few elements are present' do - allow(element_definition).to receive(:min) - .and_return(1) - allow(element_definition).to receive(:max) - .and_return('*') - allow(element_definition).to receive(:path) - .and_return('Foo.bar') - - results = validator.validate(nil, element_definition) - expect(results.validation_type).to be(:cardinality) - expect(results.is_successful).to be(false) - - results = validator.validate([], element_definition) - expect(results.validation_type).to be(:cardinality) - expect(results.is_successful).to be(false) + context 'when the cardinality is 0..1' do + let(:element_definition) do FHIR::ElementDefinition.new(min: 0, + max: 1, + id: 'Patient.id', + path: 'Patient.id') + end + it 'passes when the element has a single value' do + results = validator.validate(resource, element_definition) + expect(results).to all(have_attributes(validation_type: :cardinality)) + expect(results).to all(have_attributes(is_successful: true)) + end + it 'fails when more than one element is present' do + resource.id = [1,2] + results = validator.validate(resource, element_definition) + expect(results).to all(have_attributes(validation_type: :cardinality)) + expect(results).to all(have_attributes(is_successful: false)) + end + it 'passes when no elements are present' do + resource.id = nil + results = validator.validate(resource, element_definition) + expect(results).to all(have_attributes(validation_type: :cardinality)) + expect(results).to all(have_attributes(is_successful: true)) + end end - it 'can check for an exact number of elements to be present' do - allow(element_definition).to receive(:min) - .and_return(2) - allow(element_definition).to receive(:max) - .and_return(2) - allow(element_definition).to receive(:path) - .and_return('Foo.bar') - - results = validator.validate([element, element], element_definition) - expect(results.validation_type).to be(:cardinality) - expect(results.is_successful).to be(true) - - results = validator.validate(element, element_definition) - expect(results.validation_type).to be(:cardinality) - expect(results.is_successful).to be(false) + context 'when the cardinality is 0..*' do + let(:element_definition) do FHIR::ElementDefinition.new(min: 0, + max: '*', + id: 'Patient.id', + path: 'Patient.id') + end + it 'passes when the element has a single value' do + results = validator.validate(resource, element_definition) + expect(results).to all(have_attributes(validation_type: :cardinality)) + expect(results).to all(have_attributes(is_successful: true)) + end + it 'passes when more than one element is present' do + resource.id = [1,2] + results = validator.validate(resource, element_definition) + expect(results).to all(have_attributes(validation_type: :cardinality)) + expect(results).to all(have_attributes(is_successful: true)) + end + it 'passes when no elements are present' do + resource.id = nil + results = validator.validate(resource, element_definition) + expect(results).to all(have_attributes(validation_type: :cardinality)) + expect(results).to all(have_attributes(is_successful: true)) + end end - it 'skips cardinality on the root element' do - allow(element_definition).to receive(:min) - .and_return(0) - allow(element_definition).to receive(:max) - .and_return('*') - allow(element_definition).to receive(:path) - .and_return('Foo') + context 'when the cardinality is 1..1' do + let(:element_definition) do FHIR::ElementDefinition.new(min: 1, + max: 1, + id: 'Patient.id', + path: 'Patient.id') + end + it 'passes when the element has a single value' do + results = validator.validate(resource, element_definition) + expect(results).to all(have_attributes(validation_type: :cardinality)) + expect(results).to all(have_attributes(is_successful: true)) + end + it 'fails when more than one element is present' do + resource.id = [1,2] + results = validator.validate(resource, element_definition) + expect(results).to all(have_attributes(validation_type: :cardinality)) + expect(results).to all(have_attributes(is_successful: false)) + end + it 'fails when no elements are present' do + resource.id = nil + results = validator.validate(resource, element_definition) + expect(results).to all(have_attributes(validation_type: :cardinality)) + expect(results).to all(have_attributes(is_successful: false)) + end + end - results = validator.validate(element, element_definition) - expect(results.validation_type).to be(:cardinality) - expect(results.is_successful).to be(:skipped) + context 'when the cardinality is 1..*' do + let(:element_definition) do FHIR::ElementDefinition.new(min: 1, + max: '*', + id: 'Patient.id', + path: 'Patient.id') + end + it 'passes when the element has a single value' do + results = validator.validate(resource, element_definition) + expect(results).to all(have_attributes(validation_type: :cardinality)) + expect(results).to all(have_attributes(is_successful: true)) + end + it 'passes when more than one element is present' do + resource.id = [1,2] + results = validator.validate(resource, element_definition) + expect(results).to all(have_attributes(validation_type: :cardinality)) + expect(results).to all(have_attributes(is_successful: true)) + end + it 'fails when no elements are present' do + resource.id = nil + results = validator.validate(resource, element_definition) + expect(results).to all(have_attributes(validation_type: :cardinality)) + expect(results).to all(have_attributes(is_successful: false)) + end end end end \ No newline at end of file diff --git a/spec/unit/validation/element_validator/data_type_validator_spec.rb b/spec/unit/validation/element_validator/data_type_validator_spec.rb index 11c914c01..f1750a6cd 100644 --- a/spec/unit/validation/element_validator/data_type_validator_spec.rb +++ b/spec/unit/validation/element_validator/data_type_validator_spec.rb @@ -2,38 +2,55 @@ let(:validator) { FHIR::Validation::DataTypeValidator } - let(:element) { double(true) } - let(:element_definition) { instance_double(FHIR::ElementDefinition) } + let(:resource) do + FHIR::Patient.new(id: 2, + name: {given: 'Bob'}, + communication: [{language: 'English'}, {language: 'Spanish'}], + deceasedBoolean: false, + deceasedDateTime: 'YYYY-MM-DD') + end + + let(:element_definition) do + FHIR::ElementDefinition.new(id: 'Patient', + path: 'Patient', + type: types) + end + let(:type1) do - type1 = FHIR::ElementDefinition::Type.new - type1.code = 'boolean' - type1 + {code: 'boolean'} end + let(:type2) do - type2 = FHIR::ElementDefinition::Type.new - type2.code = 'dateTime' - type2 + # type2 = FHIR::ElementDefinition::Type.new + # type2.code = 'dateTime' + # type2 + {code: 'dateTime'} end + let(:types) do [type1, type2] end describe '#validate' do - it 'returns an array of results for elements with a choice of type' do - allow(element_definition).to receive(:path) - .and_return('Foo.bar[x]') - allow(element_definition).to receive(:type) - .and_return(types) - results = validator.validate(element, element_definition, 'Foo.barBoolean') - expect(results).to include( an_object_having_attributes(validation_type: :cardinality)) + it 'skips the root element' do + results = validator.validate(resource, element_definition) + expect(results).to be_nil + end + context 'with an element with a single type' do + let(:element_definition) do + FHIR::ElementDefinition.new(id: 'Patient.communication', + path: 'Patient.communication', + type: {code: 'HumanName'}) + end + it 'returns an array of results for the single type' do + results = validator.validate(resource, element_definition) + expect(results).to all(have_attributes(validation_type: :cardinality)) + expect(results).to all(have_attributes(is_successful: true)) + end end - it 'returns an array of results for elements with a single type' do - allow(element_definition).to receive(:path) - .and_return('Foo.bar') - allow(element_definition).to receive(:type) - .and_return([type1]) - results = validator.validate(element, element_definition, 'Foo.bar') - expect(results).to include( an_object_having_attributes(validation_type: :cardinality)) + context 'with an element that has a choice of types' do + it 'returns an array of results for elements with a choice of type' do + end end end end \ No newline at end of file From 58ba63a0a974b208185b8519b93fb280c37b1284 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Thu, 25 Jul 2019 18:57:41 -0400 Subject: [PATCH 12/80] update datatype check --- .../element_validator/data_type_validator.rb | 3 +- .../validation/profile_validator.rb | 5 +- .../data_type_validator_spec.rb | 48 ++++++++++--------- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/lib/fhir_models/validation/element_validator/data_type_validator.rb b/lib/fhir_models/validation/element_validator/data_type_validator.rb index 97d2e4c95..56385db19 100644 --- a/lib/fhir_models/validation/element_validator/data_type_validator.rb +++ b/lib/fhir_models/validation/element_validator/data_type_validator.rb @@ -44,7 +44,8 @@ def self.validate_element(element, element_definition, path) return UnknownType.new('Need path in order to determine type') unless path element_definition.type.find do |datatype| - /[^.]+$/.match(element_definition.path.gsub('[x]', datatype.code.capitalize)) == /[^.]+$/.match(path) + cap_code = "#{datatype.code[0].capitalize}#{datatype.code[1..-1]}" + /[^.]+$/.match(element_definition.path.gsub('[x]', cap_code)) == /[^.]+$/.match(path) end.code end type_def = FHIR::Definitions.type_definition(type_code) || FHIR::Definitions.resource_definition(type_code) diff --git a/lib/fhir_models/validation/profile_validator.rb b/lib/fhir_models/validation/profile_validator.rb index f59d94c37..b6460a63f 100644 --- a/lib/fhir_models/validation/profile_validator.rb +++ b/lib/fhir_models/validation/profile_validator.rb @@ -91,7 +91,8 @@ def validate(resource) element_definition = hierarchy[:elementDefinition] # Get the Results - results = @element_validators.flat_map { |validator| validator.validate(resource, element_definition, path)} + results = @element_validators.flat_map { |validator| validator.validate(resource, element_definition)} + results.compact! results.each { |res| res.profile ||= @profile.url } # Save the validation results @@ -99,6 +100,8 @@ def validate(resource) @all_results.push(*results) # Check to see if there are any valid elements to determine if we need to check the subelements + elements = FHIR::Validation::Retrieval.retrieve_by_element_definition(resource, + element_definition) element_exists = !blank?(elements.values.flatten.compact) # If the element doesn't exist we don't need to check its subelements unless we are instructed to by showskipped diff --git a/spec/unit/validation/element_validator/data_type_validator_spec.rb b/spec/unit/validation/element_validator/data_type_validator_spec.rb index f1750a6cd..db4a414a1 100644 --- a/spec/unit/validation/element_validator/data_type_validator_spec.rb +++ b/spec/unit/validation/element_validator/data_type_validator_spec.rb @@ -10,27 +10,6 @@ deceasedDateTime: 'YYYY-MM-DD') end - let(:element_definition) do - FHIR::ElementDefinition.new(id: 'Patient', - path: 'Patient', - type: types) - end - - let(:type1) do - {code: 'boolean'} - end - - let(:type2) do - # type2 = FHIR::ElementDefinition::Type.new - # type2.code = 'dateTime' - # type2 - {code: 'dateTime'} - end - - let(:types) do - [type1, type2] - end - describe '#validate' do it 'skips the root element' do results = validator.validate(resource, element_definition) @@ -38,8 +17,8 @@ end context 'with an element with a single type' do let(:element_definition) do - FHIR::ElementDefinition.new(id: 'Patient.communication', - path: 'Patient.communication', + FHIR::ElementDefinition.new(id: 'Patient.name', + path: 'Patient.name', type: {code: 'HumanName'}) end it 'returns an array of results for the single type' do @@ -49,7 +28,30 @@ end end context 'with an element that has a choice of types' do + let(:resource) do + obs = FHIR::Observation.new(valueQuantity: {value: 2, code: 'mm'}) + obs.valueCodeableConcept = FHIR::CodeableConcept.new(coding: {code: 'foo', system: 'bar'}, text: 'fake') + obs + end + let(:element_definition) do + FHIR::ElementDefinition.new(id: 'Observation.value[x]', + path: 'Observation.value[x]', + type: types) + end + + let(:type1) { {code: 'Quantity'} } + + let(:type2) { {code: 'CodeableConcept'} } + + let(:types) do + [type1, type2] + end it 'returns an array of results for elements with a choice of type' do + results = validator.validate(resource, element_definition) + expect(results).to all(have_attributes(validation_type: :cardinality)) + expect(results).to include(have_attributes(element_path: a_string_including(element_definition.type[0].code))) + expect(results).to include(have_attributes(element_path: a_string_including(element_definition.type[1].code))) + expect(results).to all(have_attributes(is_successful: true)) end end end From 99a09859638fdba52903a089796e75b1d4f5fcb4 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Fri, 26 Jul 2019 08:35:36 -0400 Subject: [PATCH 13/80] update tests --- .../validation/element_validator.rb | 89 ------------------- .../cardinality_validator_spec.rb | 12 +++ .../data_type_validator_spec.rb | 7 ++ .../terminology_validator_spec.rb | 31 +++++++ 4 files changed, 50 insertions(+), 89 deletions(-) delete mode 100644 lib/fhir_models/validation/element_validator.rb create mode 100644 spec/unit/validation/element_validator/terminology_validator_spec.rb diff --git a/lib/fhir_models/validation/element_validator.rb b/lib/fhir_models/validation/element_validator.rb deleted file mode 100644 index ec2c8de9a..000000000 --- a/lib/fhir_models/validation/element_validator.rb +++ /dev/null @@ -1,89 +0,0 @@ -module FHIR - # Element Validators accept an ElementDefinition and the associated element of the resource being validated. - module ElementValidator - # Verify that the element meets the cardinality requirements - # - # @param element [Object] The Element of the Resource under test - # @param element_definition [FHIR::ElementDefinition] The Element Definition from which the cardinality is taken - # @return result [FHIR::ValidationResult] The result of the cardinality check - def self.verify_element_cardinality(element, element_definition, current_path, skip = false) - min = element_definition.min - max = element_definition.max == '*' ? Float::INFINITY : element_definition.max.to_i - result = FHIR::ValidationResult.new - # Save a reference to the ElementDefinition - result.element_definition = element_definition - result.validation_type = :cardinality - - # Cardinality has no meaning on the first element. - # It is listed as optional(irrelevant) - # Specification Reference: http://www.hl7.org/fhir/elementdefinition.html#interpretation - # Zulip Chat: https://chat.fhir.org/#narrow/stream/179166-implementers/topic/cardinality.20of.20root.20elements/near/154024550 - if skip || !current_path.include?('.') - result.is_successful = :skipped - result.element_path = current_path || element_definition.path - return result - end - result.element_path = current_path - result.element = element - element_array = [element].flatten.compact # flatten - result.is_successful = !((element_array.size < min) || (element_array.size > max)) - result - end - - # Verify the individual FHIR Data Types - # - # FHIR Resources, Profiles and the StructureDefinitions are made up of FHIR Data Types. - # There are two kinds of structures that fall under the FHIR Data Types: complex-type and primitive-type. - # The snapshot of a resource does not contain the element definitions associated with primitive-type or complex-type - # structures it is composed of. - # - # This test validates - # - # https://www.hl7.org/fhir/datatypes.html - def self.verify_data_type(element, element_definition, current_path, skip = false) - return unless element_definition.path.include? '.' # Root Elements do not have a type - - # Can't do this validation if there is no type. - if skip || element_definition.type.empty? - result = FHIR::ValidationResult.new - result.element_definition = element_definition - result.validation_type = :datatype - result.is_successful = :skipped - result.element_path = current_path || element_definition.path - return result - end - - # Get the type - type_code = if element_definition.type.one? - element_definition.type.first.code - else - element_definition.type.find do |datatype| - /[^.]+$/.match(element_definition.path.gsub('[x]', datatype.code.capitalize)) == /[^.]+$/.match(current_path) - end.code - end - type_def = FHIR::Definitions.type_definition(type_code) || FHIR::Definitions.resource_definition(type_code) - - # If we are missing the Structure Definition needed to do the validation. - if type_def.nil? - result = FHIR::ValidationResult.new - result.element_definition = element_definition - result.validation_type = :datatype - result.is_successful = :warn - result.text = "Unkown type: #{type_code}" - result.element_path = current_path || element_definition.path - return result - end - type_validator = FHIR::ProfileValidator.new(type_def) - results = type_validator.validate(element) - - results.each { |res| res.element_path = res.element_path.gsub(/^([^.]+)/, current_path) } - end - - # Error for Unknown Types - class UnknownType < StandardError - def initialize(msg = "Unknown TypeCode") - super(msg) - end - end - end -end diff --git a/spec/unit/validation/element_validator/cardinality_validator_spec.rb b/spec/unit/validation/element_validator/cardinality_validator_spec.rb index 60497af73..552097308 100644 --- a/spec/unit/validation/element_validator/cardinality_validator_spec.rb +++ b/spec/unit/validation/element_validator/cardinality_validator_spec.rb @@ -24,18 +24,21 @@ end it 'passes when the element has a single value' do results = validator.validate(resource, element_definition) + expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) expect(results).to all(have_attributes(is_successful: true)) end it 'fails when more than one element is present' do resource.id = [1,2] results = validator.validate(resource, element_definition) + expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) expect(results).to all(have_attributes(is_successful: false)) end it 'passes when no elements are present' do resource.id = nil results = validator.validate(resource, element_definition) + expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) expect(results).to all(have_attributes(is_successful: true)) end @@ -49,18 +52,21 @@ end it 'passes when the element has a single value' do results = validator.validate(resource, element_definition) + expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) expect(results).to all(have_attributes(is_successful: true)) end it 'passes when more than one element is present' do resource.id = [1,2] results = validator.validate(resource, element_definition) + expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) expect(results).to all(have_attributes(is_successful: true)) end it 'passes when no elements are present' do resource.id = nil results = validator.validate(resource, element_definition) + expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) expect(results).to all(have_attributes(is_successful: true)) end @@ -74,18 +80,21 @@ end it 'passes when the element has a single value' do results = validator.validate(resource, element_definition) + expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) expect(results).to all(have_attributes(is_successful: true)) end it 'fails when more than one element is present' do resource.id = [1,2] results = validator.validate(resource, element_definition) + expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) expect(results).to all(have_attributes(is_successful: false)) end it 'fails when no elements are present' do resource.id = nil results = validator.validate(resource, element_definition) + expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) expect(results).to all(have_attributes(is_successful: false)) end @@ -99,18 +108,21 @@ end it 'passes when the element has a single value' do results = validator.validate(resource, element_definition) + expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) expect(results).to all(have_attributes(is_successful: true)) end it 'passes when more than one element is present' do resource.id = [1,2] results = validator.validate(resource, element_definition) + expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) expect(results).to all(have_attributes(is_successful: true)) end it 'fails when no elements are present' do resource.id = nil results = validator.validate(resource, element_definition) + expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) expect(results).to all(have_attributes(is_successful: false)) end diff --git a/spec/unit/validation/element_validator/data_type_validator_spec.rb b/spec/unit/validation/element_validator/data_type_validator_spec.rb index db4a414a1..8b6215376 100644 --- a/spec/unit/validation/element_validator/data_type_validator_spec.rb +++ b/spec/unit/validation/element_validator/data_type_validator_spec.rb @@ -11,6 +11,11 @@ end describe '#validate' do + let(:element_definition) do + FHIR::ElementDefinition.new(id: 'Patient', + path: 'Patient', + type: {code: 'Patient'}) + end it 'skips the root element' do results = validator.validate(resource, element_definition) expect(results).to be_nil @@ -23,6 +28,7 @@ end it 'returns an array of results for the single type' do results = validator.validate(resource, element_definition) + expect(results).to_not be_empty expect(results).to all(have_attributes(validation_type: :cardinality)) expect(results).to all(have_attributes(is_successful: true)) end @@ -48,6 +54,7 @@ end it 'returns an array of results for elements with a choice of type' do results = validator.validate(resource, element_definition) + expect(results).to_not be_empty expect(results).to all(have_attributes(validation_type: :cardinality)) expect(results).to include(have_attributes(element_path: a_string_including(element_definition.type[0].code))) expect(results).to include(have_attributes(element_path: a_string_including(element_definition.type[1].code))) diff --git a/spec/unit/validation/element_validator/terminology_validator_spec.rb b/spec/unit/validation/element_validator/terminology_validator_spec.rb new file mode 100644 index 000000000..328ed6b44 --- /dev/null +++ b/spec/unit/validation/element_validator/terminology_validator_spec.rb @@ -0,0 +1,31 @@ +describe FHIR::Validation::TerminologyValidator do + + let(:validator) { FHIR::Validation::TerminologyValidator.new(foo: true, bar: false) } + let(:resource) do + FHIR::Patient.new(maritalStatus: {coding: "it's complicated"}) + end + describe '#validate' do + context 'when the binding is required' do + let(:element_definition) do FHIR::ElementDefinition.new(id: 'Patient.id', + path: 'Patient.id', + binding: {strength: 'required', valueSet: 'foo'}, + type: [{code: 'CodeableConcept'}]) + end + + it 'passes when the code is in the value set' do + results = validator.validate(resource, element_definition) + expect(results.size).to eq(1) + expect(results).to all(have_attributes(validation_type: :terminology)) + expect(results).to all(have_attributes(is_successful: true)) + end + + it 'fails when the code is missing from the valueset' do + element_definition.binding.valueSet = 'bar' + results = validator.validate(resource, element_definition) + expect(results.size).to eq(1) + expect(results).to all(have_attributes(validation_type: :terminology)) + expect(results).to all(have_attributes(is_successful: false)) + end + end + end +end \ No newline at end of file From 6a185cfa9038087d8c6f3913f5ee58fd29dc6907 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Thu, 1 Aug 2019 16:09:59 -0400 Subject: [PATCH 14/80] terminology tests --- .../terminology_validator_spec.rb | 73 +++++++++++++------ 1 file changed, 50 insertions(+), 23 deletions(-) diff --git a/spec/unit/validation/element_validator/terminology_validator_spec.rb b/spec/unit/validation/element_validator/terminology_validator_spec.rb index 328ed6b44..4628a8e59 100644 --- a/spec/unit/validation/element_validator/terminology_validator_spec.rb +++ b/spec/unit/validation/element_validator/terminology_validator_spec.rb @@ -1,30 +1,57 @@ describe FHIR::Validation::TerminologyValidator do - - let(:validator) { FHIR::Validation::TerminologyValidator.new(foo: true, bar: false) } - let(:resource) do - FHIR::Patient.new(maritalStatus: {coding: "it's complicated"}) + let(:validator) do + FHIR::Validation::TerminologyValidator.new('foo' => lambda { |coding| true}, + 'bar' => lambda { |coding| false}) end - describe '#validate' do - context 'when the binding is required' do - let(:element_definition) do FHIR::ElementDefinition.new(id: 'Patient.id', - path: 'Patient.id', - binding: {strength: 'required', valueSet: 'foo'}, - type: [{code: 'CodeableConcept'}]) - end - it 'passes when the code is in the value set' do - results = validator.validate(resource, element_definition) - expect(results.size).to eq(1) - expect(results).to all(have_attributes(validation_type: :terminology)) - expect(results).to all(have_attributes(is_successful: true)) - end + data_types = {'CodeableConcept' => FHIR::CodeableConcept.new(coding: {code: 'bar', system: 'foo'}), + 'Quantity' => FHIR::Quantity.new(code: 'bar', system: 'foo'), + 'Coding' => FHIR::Coding.new(code: 'bar', system: 'foo')} + bindings = ['required', 'extensible', 'preferred', 'example'] + + data_binding_pairs = data_types.keys.product(bindings) + + shared_examples 'valueset code' do |binding_strength| + + it 'passes when the code is in the value set' do + results = validator.validate(resource, element_definition) + expect(results.size).to eq(1) + expect(results).to all(have_attributes(validation_type: :terminology)) + expect(results).to all(have_attributes(is_successful: true)) + end - it 'fails when the code is missing from the valueset' do - element_definition.binding.valueSet = 'bar' - results = validator.validate(resource, element_definition) - expect(results.size).to eq(1) - expect(results).to all(have_attributes(validation_type: :terminology)) - expect(results).to all(have_attributes(is_successful: false)) + required_strength = binding_strength == 'required' + + it "#{required_strength ? 'fails' : 'warns'} when the code is missing from the valueset" do + element_definition.binding.valueSet = 'bar' + results = validator.validate(resource, element_definition) + expect(results.size).to eq(1) + expect(results).to all(have_attributes(validation_type: :terminology)) + expect(results).to all(have_attributes(is_successful: required_strength ? false : :warn)) + end + + it 'warns when the valueset validator is missing' do + element_definition.binding.valueSet = 'baz' + results = validator.validate(resource, element_definition) + expect(results.size).to eq(1) + expect(results).to all(have_attributes(validation_type: :terminology)) + expect(results).to all(have_attributes(is_successful: :warn)) + end + end + + describe '#validate' do + data_binding_pairs.each do |data_binding_pair| + context "with a #{data_binding_pair[0]} type" do + let(:resource) { data_types[data_binding_pair[0]] } + context "with a #{data_binding_pair[1]} binding" do + let(:element_definition) do + FHIR::ElementDefinition.new(id: 'Element', + path: 'Element', + binding: {strength: data_binding_pair[1], valueSet: 'foo'}, + type: [{code: data_binding_pair[0]}]) + end + include_examples 'valueset code', data_binding_pair[1] + end end end end From 319320ef84511dd85f734e33239c6aedfa8703ea Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Thu, 1 Aug 2019 16:10:20 -0400 Subject: [PATCH 15/80] terminology validation --- .../terminology_validator.rb | 102 ++++++++++++++++++ .../validation/profile_validator.rb | 8 +- 2 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 lib/fhir_models/validation/element_validator/terminology_validator.rb diff --git a/lib/fhir_models/validation/element_validator/terminology_validator.rb b/lib/fhir_models/validation/element_validator/terminology_validator.rb new file mode 100644 index 000000000..c1efa8c57 --- /dev/null +++ b/lib/fhir_models/validation/element_validator/terminology_validator.rb @@ -0,0 +1,102 @@ +module FHIR + module Validation + class TerminologyValidator + def initialize(validators) + @vs_validators = validators + end + + # Method for registering ValueSet and CodeSystem Validators + # + # @param valueset_uri [String] The ValueSet URI the validator is related to + # @param validator [#validate] The validator + def register_vs_validator(valueset_uri, validator) + @vs_validators[valueset_uri] = validator + end + + # Verify that the element meets terminology requirements + # + # @param element [Object] The Element of the Resource under test + # @param element_definition [FHIR::ElementDefinition] The Element Definition for the elements + # @return result [FHIR::ValidationResult] The result of the terminology check + def validate(resource, element_definition) + elements = FHIR::Validation::Retrieval.retrieve_by_element_definition(resource, + element_definition, + indexed: true) + elements.flat_map do |path, el| + validate_element(el, element_definition, path) + end + end + + def validate_element(element, element_definition, path) + # Get the type + type_code = if element_definition.type.one? + element_definition.type.first.code + else + return UnknownType.new('Need path in order to determine type') unless path + + element_definition.type.find do |datatype| + cap_code = "#{datatype.code[0].capitalize}#{datatype.code[1..-1]}" + /[^.]+$/.match(element_definition.path.gsub('[x]', cap_code)) == /[^.]+$/.match(path) + end.code + end + + if ['CodeableConcept', 'Coding', 'Quantity'].include? type_code + required_strength = element_definition&.binding&.strength == 'required' + binding_issues = [] + fail_strength = required_strength ? false : :warn + + result = FHIR::ValidationResult.new + result.element_definition = element_definition + result.validation_type = :terminology + result.element_path = path + result.element = element + + valueset_uri = element_definition&.binding&.valueSet + + check_code = ->coding do + # Can't validate if both code and system are not given + if coding.code.nil? || coding.system.nil? + binding_issues << "#{path}: #{coding.to_json} missing code" if coding.code.nil? + binding_issues << "#{path}: #{coding.to_json} missing system" if coding.system.nil? + return + end + + # ValueSet Validation + check_fn = @vs_validators[valueset_uri] + has_valid_code = false + if check_fn + has_valid_code = check_fn.call(coding) + binding_issues << "#{path} has no codings from #{valueset_uri}. Codings evaluated: #{coding.to_json}" unless has_valid_code + else + binding_issues << "Missing ValueSet Validator for #{valueset_uri}" + result.is_successful = :warn + end + + # CodeSystem Validation + unless has_valid_code + check_fn = @vs_validators[coding.system] + if check_fn && !check_fn.call(coding) + binding_issues << "#{path} has no codings from it's specified system: #{coding.system}. "\ + "Codings evaluated: #{coding.to_json}" + end + end + end + + if type_code == 'CodeableConcept' + element.coding.each do |coding| + check_code.(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.(element) unless element_definition.path.include? 'CodeableConcept.coding' + end + result.text = binding_issues + + result.is_successful ||= binding_issues.empty? || fail_strength + result + end + end + end + end +end \ No newline at end of file diff --git a/lib/fhir_models/validation/profile_validator.rb b/lib/fhir_models/validation/profile_validator.rb index b6460a63f..17b431b34 100644 --- a/lib/fhir_models/validation/profile_validator.rb +++ b/lib/fhir_models/validation/profile_validator.rb @@ -127,13 +127,7 @@ def register_element_validator(element_validator) element_definition.path.split('.') end - # Method for registering ValueSet and CodeSystem Validators - # - # @param valueset_uri [String] The ValueSet URI the validator is related to - # @param validator [#validate] The validator - def register_vs_validator(valueset_uri, validator) - @vs_validators[valueset_uri] = validator - end + # This Exception is for indicating types of slices that are not handled. # From 0dc5d28f600f5e9781de6df1915bd3bd600d94eb Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Thu, 1 Aug 2019 16:38:39 -0400 Subject: [PATCH 16/80] keyword argument initialization of results --- .../element_validator/terminology_validator.rb | 9 ++++----- lib/fhir_models/validation/validation_result.rb | 7 +++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/fhir_models/validation/element_validator/terminology_validator.rb b/lib/fhir_models/validation/element_validator/terminology_validator.rb index c1efa8c57..195744d1f 100644 --- a/lib/fhir_models/validation/element_validator/terminology_validator.rb +++ b/lib/fhir_models/validation/element_validator/terminology_validator.rb @@ -45,11 +45,10 @@ def validate_element(element, element_definition, path) binding_issues = [] fail_strength = required_strength ? false : :warn - result = FHIR::ValidationResult.new - result.element_definition = element_definition - result.validation_type = :terminology - result.element_path = path - result.element = element + result = FHIR::ValidationResult.new(element_definition: element_definition, + validation_type: :terminology, + element_path: path, + element: element) valueset_uri = element_definition&.binding&.valueSet diff --git a/lib/fhir_models/validation/validation_result.rb b/lib/fhir_models/validation/validation_result.rb index 1a1bacde3..fcd5826c4 100644 --- a/lib/fhir_models/validation/validation_result.rb +++ b/lib/fhir_models/validation/validation_result.rb @@ -10,6 +10,13 @@ class ValidationResult attr_accessor :profile attr_accessor :text + def initialize(element: nil, element_definition: nil, element_path: nil, is_successful: nil, + validation_type: nil, profile: nil, text: nil) + local_variables.each do |k| + instance_variable_set("@#{k}", binding.local_variable_get(k)) + end + end + # Returns the validation result as an OperationOutcome # # @return [FHIR::OperationOutcome] The OperationOutcome From 6fd0b820337d3307f79a2fa95236ea6323b615b8 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Fri, 2 Aug 2019 18:01:05 -0400 Subject: [PATCH 17/80] change is_successful to result. simplify and test terminology better --- .../cardinality_validator.rb | 2 +- .../element_validator/data_type_validator.rb | 4 +- .../fixed_value_validator.rb | 2 +- .../terminology_validator.rb | 71 +++++---- .../validation/validation_result.rb | 4 +- .../validation/value_set_validator.rb | 10 +- .../cardinality_validator_spec.rb | 24 +-- .../data_type_validator_spec.rb | 4 +- .../fixed_value_validator_spec.rb | 4 +- .../terminology_validator_spec.rb | 139 +++++++++++++----- spec/unit/validation/validation_api_spec.rb | 2 +- 11 files changed, 167 insertions(+), 99 deletions(-) diff --git a/lib/fhir_models/validation/element_validator/cardinality_validator.rb b/lib/fhir_models/validation/element_validator/cardinality_validator.rb index c0a820600..6fbeb12dc 100644 --- a/lib/fhir_models/validation/element_validator/cardinality_validator.rb +++ b/lib/fhir_models/validation/element_validator/cardinality_validator.rb @@ -31,7 +31,7 @@ def self.validate_element(element_collection, element_definition, path) result.validation_type = :cardinality result.element_path = path result.element = element_collection - result.is_successful = !((element_collection.length < min) || (element_collection.length > max)) + result.result = !((element_collection.length < min) || (element_collection.length > max)) result end diff --git a/lib/fhir_models/validation/element_validator/data_type_validator.rb b/lib/fhir_models/validation/element_validator/data_type_validator.rb index 56385db19..5122e78c9 100644 --- a/lib/fhir_models/validation/element_validator/data_type_validator.rb +++ b/lib/fhir_models/validation/element_validator/data_type_validator.rb @@ -32,7 +32,7 @@ def self.validate_element(element, element_definition, path) result = FHIR::ValidationResult.new result.element_definition = element_definition result.validation_type = :datatype - result.is_successful = :skipped + result.result = :skipped result.element_path = path || element_definition.path return result end @@ -55,7 +55,7 @@ def self.validate_element(element, element_definition, path) result = FHIR::ValidationResult.new result.element_definition = element_definition result.validation_type = :datatype - result.is_successful = :warn + result.result = :warn result.text = "Unkown type: #{type_code}" result.element_path = path || element_definition.path return result diff --git a/lib/fhir_models/validation/element_validator/fixed_value_validator.rb b/lib/fhir_models/validation/element_validator/fixed_value_validator.rb index e502dbd04..965ef71e1 100644 --- a/lib/fhir_models/validation/element_validator/fixed_value_validator.rb +++ b/lib/fhir_models/validation/element_validator/fixed_value_validator.rb @@ -18,7 +18,7 @@ def validate(resource, element_definition, current_path = nil) result.validation_type = :fixed_value result.element = element - result.is_successful = (element == fixed) + result.result = (element == fixed) result end diff --git a/lib/fhir_models/validation/element_validator/terminology_validator.rb b/lib/fhir_models/validation/element_validator/terminology_validator.rb index 195744d1f..270ee551e 100644 --- a/lib/fhir_models/validation/element_validator/terminology_validator.rb +++ b/lib/fhir_models/validation/element_validator/terminology_validator.rb @@ -1,7 +1,7 @@ module FHIR module Validation class TerminologyValidator - def initialize(validators) + def initialize(validators = {}) @vs_validators = validators end @@ -28,6 +28,7 @@ def validate(resource, element_definition) end def validate_element(element, element_definition, path) + results = [] # Get the type type_code = if element_definition.type.one? element_definition.type.first.code @@ -40,62 +41,60 @@ def validate_element(element, element_definition, path) end.code end - if ['CodeableConcept', 'Coding', 'Quantity'].include? type_code + if %w[CodeableConcept Coding Quantity].include? type_code required_strength = element_definition&.binding&.strength == 'required' - binding_issues = [] - fail_strength = required_strength ? false : :warn - result = FHIR::ValidationResult.new(element_definition: element_definition, - validation_type: :terminology, - element_path: path, - element: element) + # result = FHIR::ValidationResult.new(element_definition: element_definition, + # validation_type: :terminology, + # element_path: path, + # element: element) + + result = lambda do |result, message| + FHIR::ValidationResult.new(element_definition: element_definition, + validation_type: :terminology, + element_path: path, + element: element, + text: message, + result: result) + end valueset_uri = element_definition&.binding&.valueSet - check_code = ->coding do + check_code = lambda do |coding, fail_strength| # Can't validate if both code and system are not given if coding.code.nil? || coding.system.nil? - binding_issues << "#{path}: #{coding.to_json} missing code" if coding.code.nil? - binding_issues << "#{path}: #{coding.to_json} missing system" if coding.system.nil? - return + results.push(result.call(:warn, "#{path}: missing code")) if coding.code.nil? + results.push(result.call(:warn, "#{path}: missing system")) if coding.system.nil? + return results end - # ValueSet Validation - check_fn = @vs_validators[valueset_uri] - has_valid_code = false - if check_fn - has_valid_code = check_fn.call(coding) - binding_issues << "#{path} has no codings from #{valueset_uri}. Codings evaluated: #{coding.to_json}" unless has_valid_code - else - binding_issues << "Missing ValueSet Validator for #{valueset_uri}" - result.is_successful = :warn - end + check = lambda do |uri, fail_level| + check_fn = @vs_validators[uri] + return result.call(:warn, "Missing Validator for #{uri}") unless check_fn - # CodeSystem Validation - unless has_valid_code - check_fn = @vs_validators[coding.system] - if check_fn && !check_fn.call(coding) - binding_issues << "#{path} has no codings from it's specified system: #{coding.system}. "\ - "Codings evaluated: #{coding.to_json}" - end + return result.call(fail_level, "#{path} has no codings from #{uri}.") unless check_fn.call(coding) + + return result.call(:pass, "#{path} has codings from #{uri}.") end + + # ValueSet Validation + results.push(check.call(valueset_uri, fail_strength)) + # Code System Validation + results.push(check.call(coding.system, :fail)) end if type_code == 'CodeableConcept' element.coding.each do |coding| - check_code.(coding) + check_code.call(coding, required_strength ? :fail : :warn) 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.(element) unless element_definition.path.include? 'CodeableConcept.coding' + check_code.call(element, required_strength ? :fail : :warn) unless element_definition.path.include? 'CodeableConcept.coding' end - result.text = binding_issues - - result.is_successful ||= binding_issues.empty? || fail_strength - result end + results end end end -end \ No newline at end of file +end diff --git a/lib/fhir_models/validation/validation_result.rb b/lib/fhir_models/validation/validation_result.rb index fcd5826c4..1862c4f3e 100644 --- a/lib/fhir_models/validation/validation_result.rb +++ b/lib/fhir_models/validation/validation_result.rb @@ -3,14 +3,14 @@ module FHIR class ValidationResult attr_accessor :element attr_accessor :element_definition - attr_accessor :is_successful + attr_accessor :result attr_accessor :validation_type attr_accessor :element_definition_id attr_accessor :element_path attr_accessor :profile attr_accessor :text - def initialize(element: nil, element_definition: nil, element_path: nil, is_successful: nil, + def initialize(element: nil, element_definition: nil, element_path: nil, result: nil, validation_type: nil, profile: nil, text: nil) local_variables.each do |k| instance_variable_set("@#{k}", binding.local_variable_get(k)) diff --git a/lib/fhir_models/validation/value_set_validator.rb b/lib/fhir_models/validation/value_set_validator.rb index c7ce01eab..382880f0e 100644 --- a/lib/fhir_models/validation/value_set_validator.rb +++ b/lib/fhir_models/validation/value_set_validator.rb @@ -58,11 +58,11 @@ def validate(element, element_definition, current_path, skip = false) result.element_path = "#{current_path}[#{index}]" result.validation_type = :terminology if coding.code.nil? || coding.system.nil? - result.is_successful = :skipped + result.result = :skipped result.text = "#{current_path}[#{index}] has no #{'code' if coding.code.nil?}" \ "#{' or' if coding.code.nil? && coding.system.nil?} #{' system' if coding.system.nil?} ." else - result.is_successful = validate_coding(coding.code, coding.system) + result.result = validate_coding(coding.code, coding.system) end results.push result end @@ -72,11 +72,11 @@ def validate(element, element_definition, current_path, skip = false) result.element_path = current_path result.validation_type = :terminology if element.code.nil? || element.system.nil? - result.is_successful = :skipped + result.result = :skipped result.text = "#{current_path} has no #{'code' if coding.code.nil?}" \ "#{' or' if coding.code.nil? && coding.system.nil?} #{' system' if coding.system.nil?} ." else - result.is_successful = validate_coding(element.code, element.system) + result.result = validate_coding(element.code, element.system) end results.push result elsif type_code == 'code' @@ -84,7 +84,7 @@ def validate(element, element_definition, current_path, skip = false) result.element_definition = element_definition result.element_path = current_path result.validation_type = :terminology - result.is_successful = :skipped + result.result = :skipped end end end diff --git a/spec/unit/validation/element_validator/cardinality_validator_spec.rb b/spec/unit/validation/element_validator/cardinality_validator_spec.rb index 552097308..c861be8b3 100644 --- a/spec/unit/validation/element_validator/cardinality_validator_spec.rb +++ b/spec/unit/validation/element_validator/cardinality_validator_spec.rb @@ -26,21 +26,21 @@ results = validator.validate(resource, element_definition) expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(is_successful: true)) + expect(results).to all(have_attributes(result: true)) end it 'fails when more than one element is present' do resource.id = [1,2] results = validator.validate(resource, element_definition) expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(is_successful: false)) + expect(results).to all(have_attributes(result: false)) end it 'passes when no elements are present' do resource.id = nil results = validator.validate(resource, element_definition) expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(is_successful: true)) + expect(results).to all(have_attributes(result: true)) end end @@ -54,21 +54,21 @@ results = validator.validate(resource, element_definition) expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(is_successful: true)) + expect(results).to all(have_attributes(result: true)) end it 'passes when more than one element is present' do resource.id = [1,2] results = validator.validate(resource, element_definition) expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(is_successful: true)) + expect(results).to all(have_attributes(result: true)) end it 'passes when no elements are present' do resource.id = nil results = validator.validate(resource, element_definition) expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(is_successful: true)) + expect(results).to all(have_attributes(result: true)) end end @@ -82,21 +82,21 @@ results = validator.validate(resource, element_definition) expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(is_successful: true)) + expect(results).to all(have_attributes(result: true)) end it 'fails when more than one element is present' do resource.id = [1,2] results = validator.validate(resource, element_definition) expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(is_successful: false)) + expect(results).to all(have_attributes(result: false)) end it 'fails when no elements are present' do resource.id = nil results = validator.validate(resource, element_definition) expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(is_successful: false)) + expect(results).to all(have_attributes(result: false)) end end @@ -110,21 +110,21 @@ results = validator.validate(resource, element_definition) expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(is_successful: true)) + expect(results).to all(have_attributes(result: true)) end it 'passes when more than one element is present' do resource.id = [1,2] results = validator.validate(resource, element_definition) expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(is_successful: true)) + expect(results).to all(have_attributes(result: true)) end it 'fails when no elements are present' do resource.id = nil results = validator.validate(resource, element_definition) expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(is_successful: false)) + expect(results).to all(have_attributes(result: false)) end end end diff --git a/spec/unit/validation/element_validator/data_type_validator_spec.rb b/spec/unit/validation/element_validator/data_type_validator_spec.rb index 8b6215376..c8219da4c 100644 --- a/spec/unit/validation/element_validator/data_type_validator_spec.rb +++ b/spec/unit/validation/element_validator/data_type_validator_spec.rb @@ -30,7 +30,7 @@ results = validator.validate(resource, element_definition) expect(results).to_not be_empty expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(is_successful: true)) + expect(results).to all(have_attributes(result: true)) end end context 'with an element that has a choice of types' do @@ -58,7 +58,7 @@ expect(results).to all(have_attributes(validation_type: :cardinality)) expect(results).to include(have_attributes(element_path: a_string_including(element_definition.type[0].code))) expect(results).to include(have_attributes(element_path: a_string_including(element_definition.type[1].code))) - expect(results).to all(have_attributes(is_successful: true)) + expect(results).to all(have_attributes(result: true)) end end end diff --git a/spec/unit/validation/element_validator/fixed_value_validator_spec.rb b/spec/unit/validation/element_validator/fixed_value_validator_spec.rb index 1ee6ac880..7f109f46a 100644 --- a/spec/unit/validation/element_validator/fixed_value_validator_spec.rb +++ b/spec/unit/validation/element_validator/fixed_value_validator_spec.rb @@ -15,7 +15,7 @@ results = validator.validate(element, element_definition) expect(results.validation_type).to be(:fixed_value) - expect(results.is_successful).to be(true) + expect(results.result).to be(true) end it 'detects when the fixed value is incorrect' do @@ -25,7 +25,7 @@ results = validator.validate(element, element_definition) expect(results.validation_type).to be(:fixed_value) - expect(results.is_successful).to be(false) + expect(results.result).to be(false) end end end \ No newline at end of file diff --git a/spec/unit/validation/element_validator/terminology_validator_spec.rb b/spec/unit/validation/element_validator/terminology_validator_spec.rb index 4628a8e59..426a00819 100644 --- a/spec/unit/validation/element_validator/terminology_validator_spec.rb +++ b/spec/unit/validation/element_validator/terminology_validator_spec.rb @@ -1,56 +1,125 @@ describe FHIR::Validation::TerminologyValidator do - let(:validator) do - FHIR::Validation::TerminologyValidator.new('foo' => lambda { |coding| true}, - 'bar' => lambda { |coding| false}) + + let(:in_system) do + lambda do + true + end end - data_types = {'CodeableConcept' => FHIR::CodeableConcept.new(coding: {code: 'bar', system: 'foo'}), - 'Quantity' => FHIR::Quantity.new(code: 'bar', system: 'foo'), - 'Coding' => FHIR::Coding.new(code: 'bar', system: 'foo')} + # let(:validator) do + # FHIR::Validation::TerminologyValidator.new('foo' => lambda { |coding| true}, + # 'bar' => lambda { |coding| false}) + # end + + # data_types = {'CodeableConcept' => FHIR::CodeableConcept.new(coding: {code: 'waldo', system: 'foo'}), + # 'Quantity' => FHIR::Quantity.new(code: 'waldo', system: 'foo'), + # 'Coding' => FHIR::Coding.new(code: 'waldo', system: 'foo')} + data_types = ['CodeableConcept', 'Quantity', 'Coding'] bindings = ['required', 'extensible', 'preferred', 'example'] - data_binding_pairs = data_types.keys.product(bindings) + data_binding_pairs = data_types.product(bindings) + + validation_options = [true, false] + validation_combinations = validation_options.product(*Array.new(3,validation_options)) - shared_examples 'valueset code' do |binding_strength| + shared_context 'ValueSet is known' do |known| + known ? let(:value_set) {'foo'} : let(:value_set) {'bar'} + end + + shared_context 'CodeSystem is known' do |known| + known ? let(:code_system) {'qux'} : let(:code_system) {'corge'} + end - it 'passes when the code is in the value set' do - results = validator.validate(resource, element_definition) - expect(results.size).to eq(1) - expect(results).to all(have_attributes(validation_type: :terminology)) - expect(results).to all(have_attributes(is_successful: true)) + shared_context 'code is in the ValueSet' do |included| + let(:value_set_validator) do + {'foo' => lambda { |coding| included}} end + end - required_strength = binding_strength == 'required' + shared_context 'code is in the CodeSystem' do |included| + let(:code_system_validator) do + {'qux' => lambda { |coding| included}} + end + end - it "#{required_strength ? 'fails' : 'warns'} when the code is missing from the valueset" do - element_definition.binding.valueSet = 'bar' - results = validator.validate(resource, element_definition) - expect(results.size).to eq(1) - expect(results).to all(have_attributes(validation_type: :terminology)) - expect(results).to all(have_attributes(is_successful: required_strength ? false : :warn)) + shared_context 'Resource type' do |resource| + let(:resource) do + case resource + when 'CodeableConcept' + FHIR::CodeableConcept.new(coding: {code: 'waldo', system: code_system}) + when 'Quantity' + FHIR::Quantity.new(code: 'waldo', system: code_system) + when 'Coding' + FHIR::Coding.new(code: 'waldo', system: code_system) + end end + end - it 'warns when the valueset validator is missing' do - element_definition.binding.valueSet = 'baz' - results = validator.validate(resource, element_definition) - expect(results.size).to eq(1) - expect(results).to all(have_attributes(validation_type: :terminology)) - expect(results).to all(have_attributes(is_successful: :warn)) + shared_context 'ElementDefinition' do |binding_strength, type| + let(:element_definition) do + FHIR::ElementDefinition.new(id: 'Element', + path: 'Element', + binding: {strength: binding_strength, valueSet: value_set}, + type: [{code: type}]) + end + end + + let(:validator) { FHIR::Validation::TerminologyValidator.new(value_set_validator.merge(code_system_validator))} + + shared_examples 'expected results' do |validation_combo, binding_strength| + required_strength = (binding_strength == 'required') + fail_strength = required_strength ? :fail : :warn + + it 'returns two results' do + expect(@results.size).to eq(2) + end + + if validation_combo[0] + it 'checks for inclusion in the ValueSet' do + expect(@results.first).to have_attributes(validation_type: :terminology) + expect(@results.first).to have_attributes(:text => a_string_including("codings from #{value_set}")) + expect(@results.first).to have_attributes(result: (validation_combo[2] ? :pass : fail_strength)) + end + else + it 'warns that the ValueSet validator is missing' do + expect(@results.first).to have_attributes(validation_type: :terminology) + expect(@results.first).to have_attributes(:text => a_string_including('Missing Validator for bar')) + expect(@results.first).to have_attributes(result: :warn) + end + end + + if validation_combo[1] + it 'checks for inclusion in the CodeSystem' do + expect(@results.last).to have_attributes(validation_type: :terminology) + expect(@results.last).to have_attributes(:text => a_string_including("codings from #{code_system}")) + expect(@results.last).to have_attributes(result: (validation_combo[3] ? :pass : :fail)) + end + else + it 'warns that the CodeSystem validator is missing' do + expect(@results.last).to have_attributes(validation_type: :terminology) + expect(@results.last).to have_attributes(:text => a_string_including('Missing Validator for corge')) + expect(@results.last).to have_attributes(result: :warn) + end end end describe '#validate' do data_binding_pairs.each do |data_binding_pair| - context "with a #{data_binding_pair[0]} type" do - let(:resource) { data_types[data_binding_pair[0]] } - context "with a #{data_binding_pair[1]} binding" do - let(:element_definition) do - FHIR::ElementDefinition.new(id: 'Element', - path: 'Element', - binding: {strength: data_binding_pair[1], valueSet: 'foo'}, - type: [{code: data_binding_pair[0]}]) + context "with a #{data_binding_pair[0]}, #{data_binding_pair[1]} binding, " do + include_context 'ElementDefinition', data_binding_pair[1], data_binding_pair[0] + include_context 'Resource type', data_binding_pair[0] + validation_combinations.each do |combo| + context "#{'un' unless combo[0]}known ValueSet #{'not ' unless combo[2]}containing the code, and "\ + "#{'un' unless combo[1]}known CodeSystem #{'not ' unless combo[3]}containing the code"do + include_context 'ValueSet is known', combo[0] + include_context 'CodeSystem is known', combo[1] + include_context 'code is in the ValueSet', combo[2] + include_context 'code is in the CodeSystem', combo[3] + before(:example) do + @results = validator.validate(resource, element_definition) + end + include_examples 'expected results', combo, data_binding_pair[1] end - include_examples 'valueset code', data_binding_pair[1] end end end diff --git a/spec/unit/validation/validation_api_spec.rb b/spec/unit/validation/validation_api_spec.rb index 287f6f716..b98c6dbec 100644 --- a/spec/unit/validation/validation_api_spec.rb +++ b/spec/unit/validation/validation_api_spec.rb @@ -53,7 +53,7 @@ it 'skips checking the cardinality of the root element' do results = validator.validate(patient_resource) - cardinality_results = results.select { |x| x.is_successful == :skipped } + cardinality_results = results.select { |x| x.result == :skipped } expect(cardinality_results).to_not be_empty end end From f6d8ef4b3a454e95e5b5fb1b77156f72ab0553fa Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Mon, 5 Aug 2019 11:22:19 -0400 Subject: [PATCH 18/80] rubocop and testing updates --- .../validation/data_type_validator.rb | 3 - .../cardinality_validator.rb | 4 +- .../element_validator/data_type_validator.rb | 5 +- .../fixed_value_validator.rb | 21 +++-- .../terminology_validator.rb | 5 +- .../validation/profile_validator.rb | 6 +- lib/fhir_models/validation/retrieval.rb | 6 +- .../validation/validation_result.rb | 7 +- .../validation/value_set_validator.rb | 90 ------------------- test/unit/validator_test.rb | 90 ------------------- 10 files changed, 26 insertions(+), 211 deletions(-) delete mode 100644 lib/fhir_models/validation/data_type_validator.rb delete mode 100644 lib/fhir_models/validation/value_set_validator.rb delete mode 100644 test/unit/validator_test.rb diff --git a/lib/fhir_models/validation/data_type_validator.rb b/lib/fhir_models/validation/data_type_validator.rb deleted file mode 100644 index 91eb9b2f1..000000000 --- a/lib/fhir_models/validation/data_type_validator.rb +++ /dev/null @@ -1,3 +0,0 @@ -class DataTypeValidator - -end \ No newline at end of file diff --git a/lib/fhir_models/validation/element_validator/cardinality_validator.rb b/lib/fhir_models/validation/element_validator/cardinality_validator.rb index 6fbeb12dc..0f176423d 100644 --- a/lib/fhir_models/validation/element_validator/cardinality_validator.rb +++ b/lib/fhir_models/validation/element_validator/cardinality_validator.rb @@ -14,7 +14,7 @@ def self.validate(resource, element_definition) # Zulip Chat: https://chat.fhir.org/#narrow/stream/179166-implementers/topic/cardinality.20of.20root.20elements/near/154024550 return unless element_definition.path.include? '.' - elements = FHIR::Validation::Retrieval.retrieve_by_element_definition(resource, + elements = Retrieval.retrieve_by_element_definition(resource, element_definition, normalized: true) elements.flat_map do |path, el| @@ -34,8 +34,6 @@ def self.validate_element(element_collection, element_definition, path) result.result = !((element_collection.length < min) || (element_collection.length > max)) result end - - private_class_method :validate_element end end end diff --git a/lib/fhir_models/validation/element_validator/data_type_validator.rb b/lib/fhir_models/validation/element_validator/data_type_validator.rb index 5122e78c9..3888a8147 100644 --- a/lib/fhir_models/validation/element_validator/data_type_validator.rb +++ b/lib/fhir_models/validation/element_validator/data_type_validator.rb @@ -2,9 +2,6 @@ module FHIR module Validation # Validator which allows Data Type validation of an element against an ElementDefinition module DataTypeValidator - - # Verify the individual FHIR Data Types - # # FHIR Resources, Profiles and the StructureDefinitions are made up of FHIR Data Types. # There are two kinds of structures that fall under the FHIR Data Types: complex-type and primitive-type. # The snapshot of a resource does not contain the element definitions associated with primitive-type or complex-type @@ -69,7 +66,7 @@ def self.validate_element(element, element_definition, path) # Error for Unknown Types class UnknownType < StandardError - def initialize(msg = "Unknown TypeCode") + def initialize(msg = 'Unknown TypeCode') super(msg) end end diff --git a/lib/fhir_models/validation/element_validator/fixed_value_validator.rb b/lib/fhir_models/validation/element_validator/fixed_value_validator.rb index 965ef71e1..371831a87 100644 --- a/lib/fhir_models/validation/element_validator/fixed_value_validator.rb +++ b/lib/fhir_models/validation/element_validator/fixed_value_validator.rb @@ -1,28 +1,35 @@ module FHIR module Validation + # Validate that an element equals the fixed value defined in the ElementDefinition module FixedValueValidator - # Validate the element matches the fixed value if a fixed value is provided # # @param element [Object] The Element of the Resource under test # @param element_definition [FHIR::ElementDefinition] The Element Definition from which the cardinality is taken # @return result [FHIR::ValidationResult] The result of the cardinality check - def validate(resource, element_definition, current_path = nil) + def self.validate(resource, element_definition) fixed = element_definition.fixed return unless fixed.respond_to?(:empty?) ? fixed.empty? : fixed.nil? - elements = retrieve_by_element_definition(resource, element_definition, indexed: true) + elements = Retrieval.retrieve_by_element_definition(resource, + element_definition, + indexed: true) + + elements.flat_map do |path, el| + validate_element(el, element_definition, path) + end + end + def self.validate_element(element, element_definition, path) result = FHIR::ValidationResult.new result.element_definition = element_definition result.validation_type = :fixed_value result.element = element + result.element_path = path - result.result = (element == fixed) + result.result = element == element_definition.fixed ? :pass : :fail result end - - module_function :validate end end -end \ No newline at end of file +end diff --git a/lib/fhir_models/validation/element_validator/terminology_validator.rb b/lib/fhir_models/validation/element_validator/terminology_validator.rb index 270ee551e..37298ce72 100644 --- a/lib/fhir_models/validation/element_validator/terminology_validator.rb +++ b/lib/fhir_models/validation/element_validator/terminology_validator.rb @@ -1,5 +1,6 @@ module FHIR module Validation + # Validate Terminology in coded elements class TerminologyValidator def initialize(validators = {}) @vs_validators = validators @@ -49,13 +50,13 @@ def validate_element(element, element_definition, path) # element_path: path, # element: element) - result = lambda do |result, message| + result = lambda do |result_status, message| FHIR::ValidationResult.new(element_definition: element_definition, validation_type: :terminology, element_path: path, element: element, text: message, - result: result) + result: result_status) end valueset_uri = element_definition&.binding&.valueSet diff --git a/lib/fhir_models/validation/profile_validator.rb b/lib/fhir_models/validation/profile_validator.rb index 17b431b34..1e3944634 100644 --- a/lib/fhir_models/validation/profile_validator.rb +++ b/lib/fhir_models/validation/profile_validator.rb @@ -85,13 +85,13 @@ def validate(resource) @snapshot_hierarchy end - private def validate_hierarchy(resource, hierarchy, skip = false) + private def validate_hierarchy(resource, hierarchy) # Validate the element hierarchy[:results] ||= [] element_definition = hierarchy[:elementDefinition] # Get the Results - results = @element_validators.flat_map { |validator| validator.validate(resource, element_definition)} + results = @element_validators.flat_map { |validator| validator.validate(resource, element_definition) } results.compact! results.each { |res| res.profile ||= @profile.url } @@ -127,8 +127,6 @@ def register_element_validator(element_validator) element_definition.path.split('.') end - - # This Exception is for indicating types of slices that are not handled. # class UnhandledSlice < StandardError diff --git a/lib/fhir_models/validation/retrieval.rb b/lib/fhir_models/validation/retrieval.rb index 9859ffc04..9100da448 100644 --- a/lib/fhir_models/validation/retrieval.rb +++ b/lib/fhir_models/validation/retrieval.rb @@ -83,9 +83,7 @@ def retrieve_by_element_definition(resource, element_definition, indexed: false, indexed_elements = {} elements.each do |k, v| v.each_with_index do |vv, kk| - if vv.url == element_definition.type.first.profile.first - indexed_elements["#{k}[#{kk}]"] = vv - end + indexed_elements["#{k}[#{kk}]"] = vv if vv.url == element_definition.type.first.profile.first end end elements = indexed_elements @@ -102,4 +100,4 @@ def self.blank?(obj) module_function :retrieve_by_element_definition, :retrieve_element_with_fhirpath end end -end \ No newline at end of file +end diff --git a/lib/fhir_models/validation/validation_result.rb b/lib/fhir_models/validation/validation_result.rb index 1862c4f3e..8501c42e2 100644 --- a/lib/fhir_models/validation/validation_result.rb +++ b/lib/fhir_models/validation/validation_result.rb @@ -10,10 +10,9 @@ class ValidationResult attr_accessor :profile attr_accessor :text - def initialize(element: nil, element_definition: nil, element_path: nil, result: nil, - validation_type: nil, profile: nil, text: nil) - local_variables.each do |k| - instance_variable_set("@#{k}", binding.local_variable_get(k)) + def initialize(**options) + options.each do |k, v| + instance_variable_set("@#{k}", v) end end diff --git a/lib/fhir_models/validation/value_set_validator.rb b/lib/fhir_models/validation/value_set_validator.rb deleted file mode 100644 index 382880f0e..000000000 --- a/lib/fhir_models/validation/value_set_validator.rb +++ /dev/null @@ -1,90 +0,0 @@ -# Provides terminology validation -class TerminologyValidator - attr_accessor :vs_validators - - def initialize - @vs_validators = {} - end - - def clear_validator(valueset_uri) - @vs_validators.delete valueset_uri - end - - def clear_all_validators() - @vs_validators = {} - end - - def add_validator(valueset_uri, &validator_fn) - @vs_validators[valueset_uri] = validator_fn - end - - # A Coding contains a system and a code - # - # The code and system will be checked against the code system itself if a valueset is not provided - def validate_coding(code, system, valueset = nil) - valueset ||= system - @vs_validators[valueset].call('system' => system, 'code' => code) - end - - def validate_code(code, valueset) - valueset ||= system - @vs_validators[valueset].call(code) - end - - def terminology_result - result = FHIR::ValidationResult.new - end - - def validate(element, element_definition, current_path, skip = false) - results = [] - binding = element_definition.binding - # TODO: Could also not return here and return a skip for elements without a binding - #return if binding.nil? - - valueSet = binding&.valueSet - - type_code = if element_definition.type.one? - element_definition.type.first.code - else - element_definition.type.find do |datatype| - /[^.]+$/.match(element_definition.path.gsub('[x]', datatype.code.capitalize)) == /[^.]+$/.match(current_path) - end&.code - end - - if type_code == 'CodeableConcept' - element.coding.each_with_index do |coding, index| - result = FHIR::ValidationResult.new - result.element_definition = element_definition - result.element_path = "#{current_path}[#{index}]" - result.validation_type = :terminology - if coding.code.nil? || coding.system.nil? - result.result = :skipped - result.text = "#{current_path}[#{index}] has no #{'code' if coding.code.nil?}" \ - "#{' or' if coding.code.nil? && coding.system.nil?} #{' system' if coding.system.nil?} ." - else - result.result = validate_coding(coding.code, coding.system) - end - results.push result - end - elsif ['Coding', 'Quantity'].include? type_code - result = FHIR::ValidationResult.new - result.element_definition = element_definition - result.element_path = current_path - result.validation_type = :terminology - if element.code.nil? || element.system.nil? - result.result = :skipped - result.text = "#{current_path} has no #{'code' if coding.code.nil?}" \ - "#{' or' if coding.code.nil? && coding.system.nil?} #{' system' if coding.system.nil?} ." - else - result.result = validate_coding(element.code, element.system) - end - results.push result - elsif type_code == 'code' - result = FHIR::ValidationResult.new - result.element_definition = element_definition - result.element_path = current_path - result.validation_type = :terminology - result.result = :skipped - end - end -end diff --git a/test/unit/validator_test.rb b/test/unit/validator_test.rb deleted file mode 100644 index b13c918f6..000000000 --- a/test/unit/validator_test.rb +++ /dev/null @@ -1,90 +0,0 @@ -require_relative '../test_helper' - -class ValidatorTest < Test::Unit::TestCase - FIXTURES_DIR = File.join('test', 'fixtures') - - class FakeValidator - def validate(resource) - return 'result' - end - end - - def setup - us_core_patient = File.join(FIXTURES_DIR, 'us_core', 'StructureDefinition-us-core-patient.json') - json = File.read(us_core_patient) - @us_core_patient_profile = FHIR.from_contents(json) - end - - def test_validator - validator = FHIR::Validator.new - assert validator.validator_modules.empty? - validator.register_validator_module(1) - assert validator.validator_modules.include? 1 - - validator.register_validator_module([3,4,5]) - assert validator.validator_modules.include? 4 - - assert validator.validator_modules.length == 4 - - validator = FHIR::Validator.new([1,2,3]) - validator.register_validator_module(1) # Should not add duplicate validators - assert validator.validator_modules.length == 3 - end - - def test_validator_validate - validator = FHIR::Validator.new - validator.register_validator_module([FakeValidator.new, FakeValidator.new]) - results = validator.validate(1) - assert results.length == 2 - end - - def test_us_core_validation - # Setup the Validator - profile_validator = FHIR::ProfileValidator.new(@us_core_patient_profile) - validator = FHIR::Validator.new(profile_validator) - - # Load the patient resource to be validated - example_name = 'invalid-Patient-example.json' - patient_record = File.join(FIXTURES_DIR, ['invalid_resources', example_name]) - json = File.read(patient_record) - resource = FHIR.from_contents(json) - - validator.show_skipped = false - results = validator.validate(resource) - assert !results.empty? - end - - # def test_profile_code_system_check - # # Clear any registered validators - # FHIR::StructureDefinition.clear_all_validates_vs - # FHIR::StructureDefinition.validates_vs "http://hl7.org/fhir/ValueSet/marital-status" do |coding| - # false # fails so that the code system validation happens - # end - # FHIR::StructureDefinition.validates_vs "http://hl7.org/fhir/v3/MaritalStatus" do |coding| - # true # no errors related to http://hl7.org/fhir/v3/MaritalStatus should be present - # end - # - # example_name = 'invalid-Patient-example.json' - # patient_record = File.join(FIXTURES_DIR, ['invalid_resources', example_name]) - # input_json = File.read(patient_record) - # resource = FHIR::Json.from_json(input_json) - # - # # validate against the declared profile - # profile = PROFILES[resource.meta.profile.first] - # profile = FHIR::Definitions.profile(resource.meta.profile.first) unless profile - # assert profile, "Failed to find profile: #{resource.meta.profile.first}" - # errors = profile.validate_resource(resource) - # errors << "Validated against #{resource.meta.profile.first}" unless errors.empty? - # - # assert !errors.empty?, 'Expected code valueset validation error.' - # assert errors.detect{|x| x.start_with?('Patient.maritalStatus has no codings from http://hl7.org/fhir/ValueSet/marital-status.')} - # assert !errors.detect{|x| x.start_with?('Patient.maritalStatus has no codings from http://hl7.org/fhir/v3/MaritalStatus.')} - # # check memory - # before = check_memory - # resource = nil - # profile = nil - # wait_for_gc - # after = check_memory - # assert_memory(before, after) - # end -end \ No newline at end of file From 0c4c45fa7742a449ebaae6d04324aaa2e1335c4b Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Mon, 5 Aug 2019 13:44:26 -0400 Subject: [PATCH 19/80] test coverage use both minittest and rspec --- .rubocop_todo.yml | 2 +- .simplecov | 8 +++++--- Rakefile | 9 ++++++--- lib/fhir_models/fhir_ext/element_definition.rb | 2 +- .../element_validator/cardinality_validator.rb | 4 ++-- .../element_validator/fixed_value_validator.rb | 6 +++--- .../validation/profile_validator.rb | 4 ++-- spec/spec_helper.rb | 2 -- .../fixed_value_validator_spec.rb | 18 ++++++++++-------- spec/unit/validation/validation_api_spec.rb | 10 ++++++---- 10 files changed, 36 insertions(+), 29 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 86c6c1576..8ea06afad 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -53,7 +53,7 @@ Metrics/BlockNesting: # Offense count: 7 # Configuration parameters: CountComments. Metrics/ClassLength: - Max: 354 + Max: 355 # Offense count: 23 Metrics/CyclomaticComplexity: diff --git a/.simplecov b/.simplecov index a555a3758..fdbf1bf1a 100644 --- a/.simplecov +++ b/.simplecov @@ -1,8 +1,10 @@ require 'simplecov' -SimpleCov.command_name 'Unit Tests' SimpleCov.start do - add_filter "test/" - add_group "Library", "lib" + add_filter 'test/' + add_group 'Library', 'lib' + add_group 'Validation', 'lib/fhir_models/validation' + add_group 'FHIR Models', 'lib/fhir_models/fhir/resources' + add_group 'Generator Files', 'lib/fhir_models/bootstrap' end class SimpleCov::Formatter::QualityFormatter diff --git a/Rakefile b/Rakefile index 0ae6678b3..491bcdda1 100644 --- a/Rakefile +++ b/Rakefile @@ -8,14 +8,17 @@ Dir['lib/fhir_models/tasks/**/*.rake'].each do |file| load file end -desc 'Run basic tests' -Rake::TestTask.new(:test) do |t| +desc 'Run minitest tests' +Rake::TestTask.new(:minitest) do |t| t.libs << 'test' t.test_files = FileList['test/**/*_test.rb'] t.verbose = true t.warning = false end +desc 'Run all tests' +task test: %i[minitest spec] + RSpec::Core::RakeTask.new desc 'Run rubocop' @@ -23,4 +26,4 @@ task :rubocop do RuboCop::RakeTask.new end -task default: %i[rubocop spec test] +task default: %i[rubocop test] diff --git a/lib/fhir_models/fhir_ext/element_definition.rb b/lib/fhir_models/fhir_ext/element_definition.rb index 59ec3fec5..a7bd74f08 100644 --- a/lib/fhir_models/fhir_ext/element_definition.rb +++ b/lib/fhir_models/fhir_ext/element_definition.rb @@ -47,7 +47,7 @@ def print_children(spaces = 0) end def choice_type? - path.end_with? '[x]' + path&.end_with? '[x]' end end end diff --git a/lib/fhir_models/validation/element_validator/cardinality_validator.rb b/lib/fhir_models/validation/element_validator/cardinality_validator.rb index 0f176423d..6bd1a2c84 100644 --- a/lib/fhir_models/validation/element_validator/cardinality_validator.rb +++ b/lib/fhir_models/validation/element_validator/cardinality_validator.rb @@ -15,8 +15,8 @@ def self.validate(resource, element_definition) return unless element_definition.path.include? '.' elements = Retrieval.retrieve_by_element_definition(resource, - element_definition, - normalized: true) + element_definition, + normalized: true) elements.flat_map do |path, el| el = [el].flatten.compact validate_element(el, element_definition, path) diff --git a/lib/fhir_models/validation/element_validator/fixed_value_validator.rb b/lib/fhir_models/validation/element_validator/fixed_value_validator.rb index 371831a87..72cfb730a 100644 --- a/lib/fhir_models/validation/element_validator/fixed_value_validator.rb +++ b/lib/fhir_models/validation/element_validator/fixed_value_validator.rb @@ -9,11 +9,11 @@ module FixedValueValidator # @return result [FHIR::ValidationResult] The result of the cardinality check def self.validate(resource, element_definition) fixed = element_definition.fixed - return unless fixed.respond_to?(:empty?) ? fixed.empty? : fixed.nil? + return if fixed.respond_to?(:empty?) ? fixed.empty? : fixed.nil? elements = Retrieval.retrieve_by_element_definition(resource, - element_definition, - indexed: true) + element_definition, + indexed: true) elements.flat_map do |path, el| validate_element(el, element_definition, path) diff --git a/lib/fhir_models/validation/profile_validator.rb b/lib/fhir_models/validation/profile_validator.rb index 1e3944634..d4d533454 100644 --- a/lib/fhir_models/validation/profile_validator.rb +++ b/lib/fhir_models/validation/profile_validator.rb @@ -108,10 +108,10 @@ def validate(resource) return unless @show_skipped || element_exists # Validate the subpath elements - hierarchy[:path].values.each { |v| validate_hierarchy(resource, v, !element_exists) } + hierarchy[:path].values.each { |v| validate_hierarchy(resource, v) } # Validate the slices elements - hierarchy[:slices].values.each { |v| validate_hierarchy(resource, v, !element_exists) } + hierarchy[:slices].values.each { |v| validate_hierarchy(resource, v) } end def register_element_validator(element_validator) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 34f1aa317..4eafefa8b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,8 +3,6 @@ require 'simplecov' SimpleCov.start do add_filter '/spec/' - add_group 'FHIR Models', 'lib/fhir_models/fhir/resources' - add_group 'Generator Files', 'lib/fhir_models/bootstrap' end require 'fhir_models' diff --git a/spec/unit/validation/element_validator/fixed_value_validator_spec.rb b/spec/unit/validation/element_validator/fixed_value_validator_spec.rb index 7f109f46a..9c11d8144 100644 --- a/spec/unit/validation/element_validator/fixed_value_validator_spec.rb +++ b/spec/unit/validation/element_validator/fixed_value_validator_spec.rb @@ -2,9 +2,12 @@ let(:validator) { FHIR::Validation::FixedValueValidator } - let(:element) { double('867-5309') } + let(:element) { '867-5309' } #let(:element_definition) { instance_double(FHIR::ElementDefinition) } - let(:element_definition) { FHIR::ElementDefinition.new } + let(:element_definition) do FHIR::ElementDefinition.new(id: 'Element', + path: 'Element', + type:[{code: 'String'}]) + end describe '#validate' do it 'returns a single result related to the fixed value' do @@ -14,18 +17,17 @@ element_definition.fixedString = element results = validator.validate(element, element_definition) - expect(results.validation_type).to be(:fixed_value) - expect(results.result).to be(true) + expect(results.first.validation_type).to be(:fixed_value) + expect(results.first.result).to be(:pass) end it 'detects when the fixed value is incorrect' do - # allow(element_definition).to receive(:fixed) - # .and_return('555-5555') + element_definition.fixedString = 'INVALID_FIXED' results = validator.validate(element, element_definition) - expect(results.validation_type).to be(:fixed_value) - expect(results.result).to be(false) + expect(results.first.validation_type).to be(:fixed_value) + expect(results.first.result).to be(:fail) end end end \ No newline at end of file diff --git a/spec/unit/validation/validation_api_spec.rb b/spec/unit/validation/validation_api_spec.rb index b98c6dbec..cb6fbf15d 100644 --- a/spec/unit/validation/validation_api_spec.rb +++ b/spec/unit/validation/validation_api_spec.rb @@ -22,14 +22,16 @@ profile_validator.register_element_validator(cardinality_validator) profile_validator.validate(resource) expect(cardinality_validator).to have_received(:validate) - .with(resource, element_definition, element_definition.path) + .with(resource, element_definition) end context 'with US Core Patient Profile Validator' do let(:us_core_profile_validator) do us_core_patient = File.join(FIXTURES_DIR, 'us_core', 'StructureDefinition-us-core-patient.json') json = File.read(us_core_patient) - FHIR::ProfileValidator.new(FHIR.from_contents(json)) + p_validator = FHIR::ProfileValidator.new(FHIR.from_contents(json)) + p_validator.register_element_validator(FHIR::Validation::CardinalityValidator) + p_validator end let(:patient_resource) do @@ -53,8 +55,8 @@ it 'skips checking the cardinality of the root element' do results = validator.validate(patient_resource) - cardinality_results = results.select { |x| x.result == :skipped } - expect(cardinality_results).to_not be_empty + root_cardinality_results = results.select { |x| x.element_path == 'Patient' } + expect(root_cardinality_results).to be_empty end end end \ No newline at end of file From 1924dadef69d2c02a186d606f2282a79ceb6be0e Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Mon, 5 Aug 2019 14:23:00 -0400 Subject: [PATCH 20/80] updating tests for better coverage and make code climate happy --- .simplecov | 1 + .../element_validator/data_type_validator.rb | 12 ++---------- .../element_validator/max_length_validator.rb | 6 ++++++ spec/spec_helper.rb | 3 --- .../element_validator/data_type_validator_spec.rb | 6 ++++++ test/test_helper.rb | 2 -- 6 files changed, 15 insertions(+), 15 deletions(-) create mode 100644 lib/fhir_models/validation/element_validator/max_length_validator.rb diff --git a/.simplecov b/.simplecov index fdbf1bf1a..c28ba3d34 100644 --- a/.simplecov +++ b/.simplecov @@ -1,6 +1,7 @@ require 'simplecov' SimpleCov.start do add_filter 'test/' + add_filter '/spec/' add_group 'Library', 'lib' add_group 'Validation', 'lib/fhir_models/validation' add_group 'FHIR Models', 'lib/fhir_models/fhir/resources' diff --git a/lib/fhir_models/validation/element_validator/data_type_validator.rb b/lib/fhir_models/validation/element_validator/data_type_validator.rb index 3888a8147..c0fc4e916 100644 --- a/lib/fhir_models/validation/element_validator/data_type_validator.rb +++ b/lib/fhir_models/validation/element_validator/data_type_validator.rb @@ -14,6 +14,8 @@ module DataTypeValidator def self.validate(resource, element_definition) return unless element_definition.path.include? '.' # Root Elements do not have a type + return if element_definition.type.empty? + elements = FHIR::Validation::Retrieval.retrieve_by_element_definition(resource, element_definition, indexed: true) @@ -24,16 +26,6 @@ def self.validate(resource, element_definition) end def self.validate_element(element, element_definition, path) - # Can't do this validation if there is no type. - if element_definition.type.empty? - result = FHIR::ValidationResult.new - result.element_definition = element_definition - result.validation_type = :datatype - result.result = :skipped - result.element_path = path || element_definition.path - return result - end - # Get the type type_code = if element_definition.type.one? element_definition.type.first.code diff --git a/lib/fhir_models/validation/element_validator/max_length_validator.rb b/lib/fhir_models/validation/element_validator/max_length_validator.rb new file mode 100644 index 000000000..56ce63648 --- /dev/null +++ b/lib/fhir_models/validation/element_validator/max_length_validator.rb @@ -0,0 +1,6 @@ +module FHIR + module Validation + module MaxLengthValidator + end + end +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4eafefa8b..d0f973efa 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,9 +1,6 @@ require 'bundler/setup' Bundler.setup require 'simplecov' -SimpleCov.start do - add_filter '/spec/' -end require 'fhir_models' # This file was generated by the `rspec --init` command. Conventionally, all diff --git a/spec/unit/validation/element_validator/data_type_validator_spec.rb b/spec/unit/validation/element_validator/data_type_validator_spec.rb index c8219da4c..ffdb38898 100644 --- a/spec/unit/validation/element_validator/data_type_validator_spec.rb +++ b/spec/unit/validation/element_validator/data_type_validator_spec.rb @@ -32,6 +32,12 @@ expect(results).to all(have_attributes(validation_type: :cardinality)) expect(results).to all(have_attributes(result: true)) end + it 'Returns a warning if the type is unknown' do + element_definition.type.first.code = 'Foo' + results = validator.validate(resource, element_definition) + expect(results).to all(have_attributes(result: :warn)) + expect(results.first).to have_attributes(text: "Unkown type: Foo") + end end context 'with an element that has a choice of types' do let(:resource) do diff --git a/test/test_helper.rb b/test/test_helper.rb index b274968e3..9fd58d8c0 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,6 +1,4 @@ require 'simplecov' -SimpleCov.start - require 'objspace' require 'nokogiri/diff' require 'test/unit' From eec14556a593a7ca19a7b88fbe7a2e68e9684b8a Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Mon, 5 Aug 2019 14:45:06 -0400 Subject: [PATCH 21/80] maxLength tests --- .../element_validator/max_length_validator.rb | 25 +++++++++- spec/spec_helper.rb | 1 + .../max_length_validator_spec.rb | 48 +++++++++++++++++++ .../terminology_validator_spec.rb | 8 ---- test/test_helper.rb | 1 + 5 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 spec/unit/validation/element_validator/max_length_validator_spec.rb diff --git a/lib/fhir_models/validation/element_validator/max_length_validator.rb b/lib/fhir_models/validation/element_validator/max_length_validator.rb index 56ce63648..780ae41ad 100644 --- a/lib/fhir_models/validation/element_validator/max_length_validator.rb +++ b/lib/fhir_models/validation/element_validator/max_length_validator.rb @@ -1,6 +1,29 @@ module FHIR module Validation + # Validate that strings do not exceed the specified max length module MaxLengthValidator + # @param element [Object] The Element of the Resource under test + # @param element_definition [FHIR::ElementDefinition] The Element Definition from which the max length is derived + # @return result [FHIR::ValidationResult] The result of the max length check + def self.validate(resource, element_definition) + return if element_definition.maxLength.nil? + + elements = Retrieval.retrieve_by_element_definition(resource, + element_definition, + indexed: true) + + elements.flat_map do |path, el| + validate_element(el, element_definition, path) + end + end + + def self.validate_element(element, element_definition, path) + FHIR::ValidationResult.new(element_definition: element_definition, + validation_type: :max_length, + element_path: path, + element: element, + result: element.length <= element_definition.maxLength ? :pass : :fail) + end end end -end \ No newline at end of file +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d0f973efa..6ea31f03b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,7 @@ require 'bundler/setup' Bundler.setup require 'simplecov' +SimpleCov.command_name 'RSpec Tests' require 'fhir_models' # This file was generated by the `rspec --init` command. Conventionally, all diff --git a/spec/unit/validation/element_validator/max_length_validator_spec.rb b/spec/unit/validation/element_validator/max_length_validator_spec.rb new file mode 100644 index 000000000..a031818d4 --- /dev/null +++ b/spec/unit/validation/element_validator/max_length_validator_spec.rb @@ -0,0 +1,48 @@ +describe FHIR::Validation::MaxLengthValidator do + let(:validator) {FHIR::Validation::MaxLengthValidator} + let(:resource) { 'foo' } + let(:element_definition) {FHIR::ElementDefinition.new(id: 'Element', path: 'Element')} + + context 'when the string is shorter than the maxLength' do + before do + element_definition.maxLength = 10 + @results = validator.validate(resource, element_definition) + end + + it 'returns one result' do + expect(@results.size).to eq(1) + end + + it 'passes' do + expect(@results.first).to have_attributes(validation_type: :max_length) + expect(@results.first).to have_attributes(result: :pass) + end + end + + context 'when the string is longer than the maxLength' do + before do + element_definition.maxLength = 2 + @results = validator.validate(resource, element_definition) + end + + it 'returns one result' do + expect(@results.size).to eq(1) + end + + it 'fails' do + expect(@results.first).to have_attributes(validation_type: :max_length) + expect(@results.first).to have_attributes(result: :fail) + end + end + + context 'when no max length is specified' do + before do + element_definition.maxLength = nil + @results = validator.validate(resource, element_definition) + end + + it 'returns no results' do + expect(@results).to be_nil + end + end +end \ No newline at end of file diff --git a/spec/unit/validation/element_validator/terminology_validator_spec.rb b/spec/unit/validation/element_validator/terminology_validator_spec.rb index 426a00819..4e4ae9a8b 100644 --- a/spec/unit/validation/element_validator/terminology_validator_spec.rb +++ b/spec/unit/validation/element_validator/terminology_validator_spec.rb @@ -6,14 +6,6 @@ end end - # let(:validator) do - # FHIR::Validation::TerminologyValidator.new('foo' => lambda { |coding| true}, - # 'bar' => lambda { |coding| false}) - # end - - # data_types = {'CodeableConcept' => FHIR::CodeableConcept.new(coding: {code: 'waldo', system: 'foo'}), - # 'Quantity' => FHIR::Quantity.new(code: 'waldo', system: 'foo'), - # 'Coding' => FHIR::Coding.new(code: 'waldo', system: 'foo')} data_types = ['CodeableConcept', 'Quantity', 'Coding'] bindings = ['required', 'extensible', 'preferred', 'example'] diff --git a/test/test_helper.rb b/test/test_helper.rb index 9fd58d8c0..8772ba759 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,4 +1,5 @@ require 'simplecov' +SimpleCov.command_name 'Minitest Tests' require 'objspace' require 'nokogiri/diff' require 'test/unit' From 5dcaf1d80b6bedbaf83bec2bc8155af893deadbc Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Mon, 5 Aug 2019 14:57:44 -0400 Subject: [PATCH 22/80] trying to make code-climate understand --- .simplecov | 1 + 1 file changed, 1 insertion(+) diff --git a/.simplecov b/.simplecov index c28ba3d34..05e1fa942 100644 --- a/.simplecov +++ b/.simplecov @@ -1,4 +1,5 @@ require 'simplecov' +SimpleCov.command_name 'All Tests' SimpleCov.start do add_filter 'test/' add_filter '/spec/' From 61b9c40c236c5c2d0483b19dcdf1ec1cbee110ac Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Mon, 5 Aug 2019 15:34:49 -0400 Subject: [PATCH 23/80] code climate update needs to downgrade codecov --- Gemfile.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 82a614445..3d5153bb3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,15 +13,15 @@ GEM ast (2.4.0) bcp47 (0.3.3) i18n - codeclimate-test-reporter (1.0.7) - simplecov + codeclimate-test-reporter (1.0.9) + simplecov (<= 0.13) coderay (1.1.2) concurrent-ruby (1.1.5) coolline (0.5.0) unicode_utils (~> 1.4) date_time_precision (0.8.1) diff-lcs (1.3) - docile (1.3.1) + docile (1.1.5) ffi (1.10.0) formatador (0.2.5) guard (2.15.0) @@ -103,8 +103,8 @@ GEM ruby-progressbar (1.10.0) ruby_dep (1.5.0) shellany (0.0.1) - simplecov (0.16.1) - docile (~> 1.1) + simplecov (0.13.0) + docile (~> 1.1.0) json (>= 1.8, < 3) simplecov-html (~> 0.10.0) simplecov-html (0.10.2) From f4ebee750f862b480c67b2c16991f4975c6b5558 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Tue, 6 Aug 2019 11:13:30 -0400 Subject: [PATCH 24/80] update cardinality to pass fail --- .../cardinality_validator.rb | 2 +- .../terminology_validator.rb | 5 ---- .../cardinality_validator_spec.rb | 24 +++++++++---------- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/lib/fhir_models/validation/element_validator/cardinality_validator.rb b/lib/fhir_models/validation/element_validator/cardinality_validator.rb index 6bd1a2c84..21ceed852 100644 --- a/lib/fhir_models/validation/element_validator/cardinality_validator.rb +++ b/lib/fhir_models/validation/element_validator/cardinality_validator.rb @@ -31,7 +31,7 @@ def self.validate_element(element_collection, element_definition, path) result.validation_type = :cardinality result.element_path = path result.element = element_collection - result.result = !((element_collection.length < min) || (element_collection.length > max)) + result.result = ((element_collection.length < min) || (element_collection.length > max)) ? :fail : :pass result end end diff --git a/lib/fhir_models/validation/element_validator/terminology_validator.rb b/lib/fhir_models/validation/element_validator/terminology_validator.rb index 37298ce72..b0179cdde 100644 --- a/lib/fhir_models/validation/element_validator/terminology_validator.rb +++ b/lib/fhir_models/validation/element_validator/terminology_validator.rb @@ -45,11 +45,6 @@ def validate_element(element, element_definition, path) if %w[CodeableConcept Coding Quantity].include? type_code required_strength = element_definition&.binding&.strength == 'required' - # result = FHIR::ValidationResult.new(element_definition: element_definition, - # validation_type: :terminology, - # element_path: path, - # element: element) - result = lambda do |result_status, message| FHIR::ValidationResult.new(element_definition: element_definition, validation_type: :terminology, diff --git a/spec/unit/validation/element_validator/cardinality_validator_spec.rb b/spec/unit/validation/element_validator/cardinality_validator_spec.rb index c861be8b3..f0dbdb7c7 100644 --- a/spec/unit/validation/element_validator/cardinality_validator_spec.rb +++ b/spec/unit/validation/element_validator/cardinality_validator_spec.rb @@ -26,21 +26,21 @@ results = validator.validate(resource, element_definition) expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(result: true)) + expect(results).to all(have_attributes(result: :pass)) end it 'fails when more than one element is present' do resource.id = [1,2] results = validator.validate(resource, element_definition) expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(result: false)) + expect(results).to all(have_attributes(result: :fail)) end it 'passes when no elements are present' do resource.id = nil results = validator.validate(resource, element_definition) expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(result: true)) + expect(results).to all(have_attributes(result: :pass)) end end @@ -54,21 +54,21 @@ results = validator.validate(resource, element_definition) expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(result: true)) + expect(results).to all(have_attributes(result: :pass)) end it 'passes when more than one element is present' do resource.id = [1,2] results = validator.validate(resource, element_definition) expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(result: true)) + expect(results).to all(have_attributes(result: :pass)) end it 'passes when no elements are present' do resource.id = nil results = validator.validate(resource, element_definition) expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(result: true)) + expect(results).to all(have_attributes(result: :pass)) end end @@ -82,21 +82,21 @@ results = validator.validate(resource, element_definition) expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(result: true)) + expect(results).to all(have_attributes(result: :pass)) end it 'fails when more than one element is present' do resource.id = [1,2] results = validator.validate(resource, element_definition) expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(result: false)) + expect(results).to all(have_attributes(result: :fail)) end it 'fails when no elements are present' do resource.id = nil results = validator.validate(resource, element_definition) expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(result: false)) + expect(results).to all(have_attributes(result: :fail)) end end @@ -110,21 +110,21 @@ results = validator.validate(resource, element_definition) expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(result: true)) + expect(results).to all(have_attributes(result: :pass)) end it 'passes when more than one element is present' do resource.id = [1,2] results = validator.validate(resource, element_definition) expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(result: true)) + expect(results).to all(have_attributes(result: :pass)) end it 'fails when no elements are present' do resource.id = nil results = validator.validate(resource, element_definition) expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(result: false)) + expect(results).to all(have_attributes(result: :fail)) end end end From cf84d06d2090cd21d6897d7a1105078d0ef32690 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Tue, 6 Aug 2019 11:45:01 -0400 Subject: [PATCH 25/80] cleanup and appease rubocop --- .../element_validator/cardinality_validator.rb | 13 ++++++------- .../element_validator/data_type_validator.rb | 12 +++++------- .../element_validator/fixed_value_validator.rb | 13 +++++-------- .../element_validator/data_type_validator_spec.rb | 6 +++--- 4 files changed, 19 insertions(+), 25 deletions(-) diff --git a/lib/fhir_models/validation/element_validator/cardinality_validator.rb b/lib/fhir_models/validation/element_validator/cardinality_validator.rb index 21ceed852..d9d95d063 100644 --- a/lib/fhir_models/validation/element_validator/cardinality_validator.rb +++ b/lib/fhir_models/validation/element_validator/cardinality_validator.rb @@ -26,13 +26,12 @@ def self.validate(resource, element_definition) def self.validate_element(element_collection, element_definition, path) min = element_definition.min max = element_definition.max == '*' ? Float::INFINITY : element_definition.max.to_i - result = FHIR::ValidationResult.new - result.element_definition = element_definition - result.validation_type = :cardinality - result.element_path = path - result.element = element_collection - result.result = ((element_collection.length < min) || (element_collection.length > max)) ? :fail : :pass - result + cardinality_check = ((element_collection.length < min) || (element_collection.length > max)) + FHIR::ValidationResult.new(element_definition: element_definition, + validation_type: :cardinality, + element_path: path, + element: element_collection, + result: cardinality_check ? :fail : :pass) end end end diff --git a/lib/fhir_models/validation/element_validator/data_type_validator.rb b/lib/fhir_models/validation/element_validator/data_type_validator.rb index c0fc4e916..71cae8123 100644 --- a/lib/fhir_models/validation/element_validator/data_type_validator.rb +++ b/lib/fhir_models/validation/element_validator/data_type_validator.rb @@ -41,13 +41,11 @@ def self.validate_element(element, element_definition, path) # If we are missing the Structure Definition needed to do the validation. if type_def.nil? - result = FHIR::ValidationResult.new - result.element_definition = element_definition - result.validation_type = :datatype - result.result = :warn - result.text = "Unkown type: #{type_code}" - result.element_path = path || element_definition.path - return result + return FHIR::ValidationResult.new(element_definition: element_definition, + validation_type: :datatype, + result: :warn, + text: "Unknown type: #{type_code}", + element_path: path || element_definition.path) end type_validator = FHIR::ProfileValidator.new(type_def) type_validator.register_element_validator(FHIR::Validation::CardinalityValidator) diff --git a/lib/fhir_models/validation/element_validator/fixed_value_validator.rb b/lib/fhir_models/validation/element_validator/fixed_value_validator.rb index 72cfb730a..5be37b24a 100644 --- a/lib/fhir_models/validation/element_validator/fixed_value_validator.rb +++ b/lib/fhir_models/validation/element_validator/fixed_value_validator.rb @@ -21,14 +21,11 @@ def self.validate(resource, element_definition) end def self.validate_element(element, element_definition, path) - result = FHIR::ValidationResult.new - result.element_definition = element_definition - result.validation_type = :fixed_value - result.element = element - result.element_path = path - - result.result = element == element_definition.fixed ? :pass : :fail - result + FHIR::ValidationResult.new(element_definition: element_definition, + validation_type: :fixed_value, + element: element, + element_path: path, + result: element == element_definition.fixed ? :pass : :fail) end end end diff --git a/spec/unit/validation/element_validator/data_type_validator_spec.rb b/spec/unit/validation/element_validator/data_type_validator_spec.rb index ffdb38898..0c3bdd68f 100644 --- a/spec/unit/validation/element_validator/data_type_validator_spec.rb +++ b/spec/unit/validation/element_validator/data_type_validator_spec.rb @@ -30,13 +30,13 @@ results = validator.validate(resource, element_definition) expect(results).to_not be_empty expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(result: true)) + expect(results).to all(have_attributes(result: :pass)) end it 'Returns a warning if the type is unknown' do element_definition.type.first.code = 'Foo' results = validator.validate(resource, element_definition) expect(results).to all(have_attributes(result: :warn)) - expect(results.first).to have_attributes(text: "Unkown type: Foo") + expect(results.first).to have_attributes(text: "Unknown type: Foo") end end context 'with an element that has a choice of types' do @@ -64,7 +64,7 @@ expect(results).to all(have_attributes(validation_type: :cardinality)) expect(results).to include(have_attributes(element_path: a_string_including(element_definition.type[0].code))) expect(results).to include(have_attributes(element_path: a_string_including(element_definition.type[1].code))) - expect(results).to all(have_attributes(result: true)) + expect(results).to all(have_attributes(result: :pass)) end end end From e2cd931feee0561126669cff197647470583c62e Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Wed, 7 Aug 2019 09:42:51 -0400 Subject: [PATCH 26/80] structured results --- .../element_validator/data_type_validator.rb | 2 +- .../validation/profile_validator.rb | 138 ----------------- lib/fhir_models/validation/retrieval.rb | 2 +- .../validation/structure_validation_result.rb | 49 ++++++ .../validation/structure_validator.rb | 145 ++++++++++++++++++ .../validation/structure_validator_spec.rb | 44 ++++++ spec/unit/validation/validation_api_spec.rb | 5 +- 7 files changed, 243 insertions(+), 142 deletions(-) delete mode 100644 lib/fhir_models/validation/profile_validator.rb create mode 100644 lib/fhir_models/validation/structure_validation_result.rb create mode 100644 lib/fhir_models/validation/structure_validator.rb create mode 100644 spec/unit/validation/structure_validator_spec.rb diff --git a/lib/fhir_models/validation/element_validator/data_type_validator.rb b/lib/fhir_models/validation/element_validator/data_type_validator.rb index 71cae8123..9fc8195d6 100644 --- a/lib/fhir_models/validation/element_validator/data_type_validator.rb +++ b/lib/fhir_models/validation/element_validator/data_type_validator.rb @@ -47,7 +47,7 @@ def self.validate_element(element, element_definition, path) text: "Unknown type: #{type_code}", element_path: path || element_definition.path) end - type_validator = FHIR::ProfileValidator.new(type_def) + type_validator = FHIR::Validation::StructureValidator.new(type_def) type_validator.register_element_validator(FHIR::Validation::CardinalityValidator) results = type_validator.validate(element) diff --git a/lib/fhir_models/validation/profile_validator.rb b/lib/fhir_models/validation/profile_validator.rb deleted file mode 100644 index d4d533454..000000000 --- a/lib/fhir_models/validation/profile_validator.rb +++ /dev/null @@ -1,138 +0,0 @@ -module FHIR - # Validator which allows validation of a resource against a profile - class ProfileValidator - @vs_validators = {} - - attr_accessor :all_results - attr_accessor :show_skipped - attr_accessor :element_validators - - # Create a ProfileValidator from a FHIR::StructureDefinition - # - # @param profile [FHIR::StructureDefinition] - # - def initialize(profile) - @profile = profile - @all_results = [] - @show_skipped = true - @element_validators = [] - end - - # Validate the provided resource - # - # @param resource [FHIR::Model] The Resource to be validated - # @return [Hash] the validation results - def validate(resource) - validate_against_hierarchy(resource) - @all_results - end - - # Build a hierarchy from a list of ElementDefinitions - # - # @param [Array] List of Element Definitions - # @return [Hash] The ElementDefinition hierarchy - private def build_hierarchy(elem_def_list) - hierarchy = {} - elem_def_list.each do |element| - # Separate path and slices into an array of keys - slices = element.id.split(':') - path = slices.shift.split('.') - slices = slices.pop&.split('/') - last_path = slices.nil? ? path.pop : slices.pop - - # Build the hierarchy - thing = path.inject(hierarchy) do |memo, k| - memo[k] ||= { elementDefinition: nil, path: {}, slices: {} } - memo[k][:path] - end - - # If there are slices - unless slices.nil? - path_down = path.zip(Array.new(path.length - 1, :path)).push(:slices).flatten.compact - thing = slices.inject(hierarchy.dig(*path_down)) do |memo, k| - memo[k] ||= { elementDefinition: nil, path: {}, slices: {} } - memo[k][:slices] - end - end - - # If there are no slices - thing[last_path] = { elementDefinition: element, path: {}, slices: {} } - end - hierarchy - # traverse(hierarchy) { |x| puts x.id } - end - - # Traverse the hierarchy - # - # @param hierarchy [Hash] element definition hierarchy - # @yield [elementDefinition] provides the element definition to the block - private def traverse(hierarchy, &block) - hierarchy.each do |_, v| - # yield that element definition - yield(v[:elementDefinition]) - traverse(v[:slices], &block) unless v[:slices].empty? - traverse(v[:path], &block) unless v[:path].empty? - end - end - - private def validate_against_hierarchy(resource) - @snapshot_hierarchy ||= build_hierarchy(@profile.snapshot.element) - # Slicing is prohibited on first element so we only check the paths - # http://www.hl7.org/fhir/elementdefinition.html#interpretation - @snapshot_hierarchy.values.each do |value| - validate_hierarchy(resource, value) - end - @snapshot_hierarchy - end - - private def validate_hierarchy(resource, hierarchy) - # Validate the element - hierarchy[:results] ||= [] - element_definition = hierarchy[:elementDefinition] - - # Get the Results - results = @element_validators.flat_map { |validator| validator.validate(resource, element_definition) } - results.compact! - results.each { |res| res.profile ||= @profile.url } - - # Save the validation results - hierarchy[:results].push(*results) - @all_results.push(*results) - - # Check to see if there are any valid elements to determine if we need to check the subelements - elements = FHIR::Validation::Retrieval.retrieve_by_element_definition(resource, - element_definition) - element_exists = !blank?(elements.values.flatten.compact) - - # If the element doesn't exist we don't need to check its subelements unless we are instructed to by showskipped - return unless @show_skipped || element_exists - - # Validate the subpath elements - hierarchy[:path].values.each { |v| validate_hierarchy(resource, v) } - - # Validate the slices elements - hierarchy[:slices].values.each { |v| validate_hierarchy(resource, v) } - end - - def register_element_validator(element_validator) - @element_validators.push(element_validator) - end - - private def blank?(obj) - obj.respond_to?(:empty?) ? obj.empty? : obj.nil? - end - - # Splits a path into an array - private def element_path_array(element_definition) - element_definition.path.split('.') - end - - # This Exception is for indicating types of slices that are not handled. - # - class UnhandledSlice < StandardError - def initialize(msg = 'Unhandled Slice') - super(msg) - end - end - end -end diff --git a/lib/fhir_models/validation/retrieval.rb b/lib/fhir_models/validation/retrieval.rb index 9100da448..42f8f8593 100644 --- a/lib/fhir_models/validation/retrieval.rb +++ b/lib/fhir_models/validation/retrieval.rb @@ -16,7 +16,7 @@ def retrieve_element_with_fhirpath(path, resource, indexed = true) desired_elements = spath.inject(fhirpath_elements) do |memo, meth| digging = {} memo.each do |k, v| - elms = v.send(meth) + elms = v.send(meth) if v.is_a? FHIR::Model # FHIR Primitives are not modeled and will throw NoMethod Error # More than one element where the FHIRPath needs indexing if elms.respond_to? :each_with_index elms.each_with_index do |vv, kk| diff --git a/lib/fhir_models/validation/structure_validation_result.rb b/lib/fhir_models/validation/structure_validation_result.rb new file mode 100644 index 000000000..ae439a69b --- /dev/null +++ b/lib/fhir_models/validation/structure_validation_result.rb @@ -0,0 +1,49 @@ +#require_relative 'traverse' +module FHIR + module Validation + class StructureValidationResult + + def initialize(validation_hash) + @validation_hash = validation_hash + end + + def traverse_results(&block) + traverse(@validation_hash, :results, &block) + end + + def traverse_element_definition(&block) + traverse(@validation_hash, :element_definition, &block) + end + + def all_results + all_validation_results(@validation_hash) + end + + def results_for_path(path) + slices = path.split(':') + path = slices.shift.splt('.') + hierarchy = @validation_hash[path.shift.to_sym] + path.inject(hierarchy) do |memo, k| + memo[:path][k.to_sym] + end[:results] + end + + private def traverse(hierarchy, desired_key, &block) + hierarchy.each do |_, v| + # yield the desired value + yield(v[:desired_key]) + traverse(v[:slices], desired_key, &block) unless v[:slices].empty? + traverse(v[:path], desired_key, &block) unless v[:path].empty? + end + end + + private def all_validation_results(hierarchy) + hierarchy.flat_map do |_, v| + slice_results = v[:slices].empty? ? [] : all_validation_results(v[:slices]) + sub_element_results = v[:path].empty? ? [] : all_validation_results(v[:path]) + v[:results].concat(slice_results, sub_element_results) + end + end + end + end +end \ No newline at end of file diff --git a/lib/fhir_models/validation/structure_validator.rb b/lib/fhir_models/validation/structure_validator.rb new file mode 100644 index 000000000..795004add --- /dev/null +++ b/lib/fhir_models/validation/structure_validator.rb @@ -0,0 +1,145 @@ +module FHIR + module Validation + # Validator which allows validation of a resource against a profile + class StructureValidator + @vs_validators = {} + + attr_accessor :all_results + attr_accessor :show_skipped + attr_reader :element_validators + + # Create a StructureValidator from a FHIR::StructureDefinition + # + # @param profile [FHIR::StructureDefinition] + # + def initialize(profile, use_default_element_validators: true) + @profile = profile + @show_skipped = true + @element_validators = Set.new + add_default_element_validators if use_default_element_validators + end + + # Validate the provided resource + # + # @param resource [FHIR::Model] The Resource to be validated + # @return [Hash] the validation results + def validate(resource) + validate_against_hierarchy(resource) + FHIR::Validation::StructureValidationResult.new(@snapshot_hierarchy) + end + + # Removes the currently registered element validators + def clear_element_validators + @element_validators = Set.new + end + + # Add Element Validators to the StructureValidator + # + # @param element_validators [Enumerable] + def register_element_validators(element_validators) + @element_validators.merge(Set.new(element_validators)) + end + + # Add the default Element Validators to the StructureValidator + def add_default_element_validators + @element_validators.merge(Set[Validation::CardinalityValidator, + Validation::DataTypeValidator, + Validation::FixedValueValidator, + Validation::MaxLengthValidator]) + end + + # Build a hierarchy from a list of ElementDefinitions + # + # @param [Array] List of Element Definitions + # @return [Hash] The ElementDefinition hierarchy + private def build_hierarchy(elem_def_list) + hierarchy = {} + elem_def_list.each do |element| + # Separate path and slices into an array of keys + slices = element.id.split(':') + path = slices.shift.split('.') + slices = slices.pop&.split('/') + last_path = slices.nil? ? path.pop : slices.pop + + # Build the hierarchy + thing = path.inject(hierarchy) do |memo, k| + memo[k] ||= { elementDefinition: nil, path: {}, slices: {} } + memo[k][:path] + end + + # If there are slices + unless slices.nil? + path_down = path.zip(Array.new(path.length - 1, :path)).push(:slices).flatten.compact + thing = slices.inject(hierarchy.dig(*path_down)) do |memo, k| + memo[k] ||= { elementDefinition: nil, path: {}, slices: {} } + memo[k][:slices] + end + end + + # If there are no slices + thing[last_path] = { elementDefinition: element, path: {}, slices: {} } + end + hierarchy + end + + private def validate_against_hierarchy(resource) + @snapshot_hierarchy ||= build_hierarchy(@profile.snapshot.element) + # Slicing is prohibited on first element so we only check the paths + # http://www.hl7.org/fhir/elementdefinition.html#interpretation + @snapshot_hierarchy.values.each do |value| + validate_hierarchy(resource, value) + end + @snapshot_hierarchy + end + + private def validate_hierarchy(resource, hierarchy) + # Validate the element + hierarchy[:results] ||= [] + element_definition = hierarchy[:elementDefinition] + + # Get the Results + results = @element_validators.flat_map { |validator| validator.validate(resource, element_definition) } + results.compact! + results.each { |res| res.profile ||= @profile.url } + + # Save the validation results + hierarchy[:results].push(*results) + + # Check to see if there are any valid elements to determine if we need to check the subelements + elements = FHIR::Validation::Retrieval.retrieve_by_element_definition(resource, + element_definition) + element_exists = !blank?(elements.values.flatten.compact) + + # If the element doesn't exist we don't need to check its subelements unless we are instructed to by showskipped + return unless @show_skipped || element_exists + + # Validate the subpath elements + hierarchy[:path].values.each { |v| validate_hierarchy(resource, v) } + + # Validate the slices elements + hierarchy[:slices].values.each { |v| validate_hierarchy(resource, v) } + end + + def register_element_validator(element_validator) + @element_validators.add(*element_validator) + end + + private def blank?(obj) + obj.respond_to?(:empty?) ? obj.empty? : obj.nil? + end + + # Splits a path into an array + private def element_path_array(element_definition) + element_definition.path.split('.') + end + + # This Exception is for indicating types of slices that are not handled. + # + class UnhandledSlice < StandardError + def initialize(msg = 'Unhandled Slice') + super(msg) + end + end + end + end +end diff --git a/spec/unit/validation/structure_validator_spec.rb b/spec/unit/validation/structure_validator_spec.rb new file mode 100644 index 000000000..5ed0513b4 --- /dev/null +++ b/spec/unit/validation/structure_validator_spec.rb @@ -0,0 +1,44 @@ +describe FHIR::Validation::StructureValidator do + + let(:profile) {FHIR::StructureDefinition.new} + let(:validator) do + FHIR::Validation::StructureValidator.new(profile) + end + + context + context 'with US Core Patient Profile Validator' do + let(:us_core_profile_validator) do + us_core_patient = File.join(FIXTURES_DIR, 'us_core', 'StructureDefinition-us-core-patient.json') + json = File.read(us_core_patient) + p_validator = FHIR::Validation::StructureValidator.new(FHIR.from_contents(json)) + #p_validator.register_element_validator(FHIR::Validation::CardinalityValidator) + p_validator + end + + let(:patient_resource) do + patient_record = File.join(FIXTURES_DIR, ['invalid_resources', 'invalid-Patient-example.json']) + json = File.read(patient_record) + FHIR.from_contents(json) + end + + before { validator.register_validator_module(us_core_profile_validator) } + + it '#validate' do + results = validator.validate(patient_resource) + results.first.all_results + expect(results).to_not be_empty + end + + it 'checks element cardinality' do + results = validator.validate(patient_resource) + cardinality_results = results.select { |x| x.validation_type == :cardinality } + expect(cardinality_results).to_not be_empty + end + + it 'skips checking the cardinality of the root element' do + results = validator.validate(patient_resource) + root_cardinality_results = results.select { |x| x.element_path == 'Patient' } + expect(root_cardinality_results).to be_empty + end + end +end \ No newline at end of file diff --git a/spec/unit/validation/validation_api_spec.rb b/spec/unit/validation/validation_api_spec.rb index cb6fbf15d..68b6e1215 100644 --- a/spec/unit/validation/validation_api_spec.rb +++ b/spec/unit/validation/validation_api_spec.rb @@ -2,7 +2,7 @@ FIXTURES_DIR = File.join('test', 'fixtures') let(:validator) {FHIR::Validator.new} - let(:profile_validator) { FHIR::ProfileValidator.new(structure_definition)} + let(:profile_validator) { FHIR::Validation::StructureValidator.new(structure_definition)} let(:cardinality_validator) { spy(FHIR::Validation::CardinalityValidator)} let(:element_definition) do element_definition = FHIR::ElementDefinition.new(id: 'Patient', path: 'Patient') @@ -29,7 +29,7 @@ let(:us_core_profile_validator) do us_core_patient = File.join(FIXTURES_DIR, 'us_core', 'StructureDefinition-us-core-patient.json') json = File.read(us_core_patient) - p_validator = FHIR::ProfileValidator.new(FHIR.from_contents(json)) + p_validator = FHIR::Validation::StructureValidator.new(FHIR.from_contents(json)) p_validator.register_element_validator(FHIR::Validation::CardinalityValidator) p_validator end @@ -44,6 +44,7 @@ it '#validate' do results = validator.validate(patient_resource) + results.first.all_results expect(results).to_not be_empty end From f124d295517ffe0cde1b4fb55ff5931e914f91fa Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Wed, 7 Aug 2019 11:36:42 -0400 Subject: [PATCH 27/80] update retrieval --- .../element_validator/data_type_validator.rb | 8 +- lib/fhir_models/validation/retrieval.rb | 6 +- .../validation/structure_validation_result.rb | 2 + .../validation/structure_validator.rb | 15 +-- .../validation/structure_validator_spec.rb | 107 ++++++++++++------ spec/unit/validation/validation_api_spec.rb | 63 ----------- 6 files changed, 88 insertions(+), 113 deletions(-) delete mode 100644 spec/unit/validation/validation_api_spec.rb diff --git a/lib/fhir_models/validation/element_validator/data_type_validator.rb b/lib/fhir_models/validation/element_validator/data_type_validator.rb index 9fc8195d6..e2db21136 100644 --- a/lib/fhir_models/validation/element_validator/data_type_validator.rb +++ b/lib/fhir_models/validation/element_validator/data_type_validator.rb @@ -48,10 +48,14 @@ def self.validate_element(element, element_definition, path) element_path: path || element_definition.path) end type_validator = FHIR::Validation::StructureValidator.new(type_def) - type_validator.register_element_validator(FHIR::Validation::CardinalityValidator) + type_validator.register_element_validators(FHIR::Validation::CardinalityValidator) results = type_validator.validate(element) - results.each { |res| res.element_path = res.element_path.gsub(/^([^.]+)/, path) } + # Update the path to reflect the object it is nested within + results.map do |res| + res.element_path = res.element_path.gsub(/^([^.]+)/, path) + res + end end # Error for Unknown Types diff --git a/lib/fhir_models/validation/retrieval.rb b/lib/fhir_models/validation/retrieval.rb index 42f8f8593..7b7355eb1 100644 --- a/lib/fhir_models/validation/retrieval.rb +++ b/lib/fhir_models/validation/retrieval.rb @@ -80,13 +80,11 @@ def retrieve_by_element_definition(resource, element_definition, indexed: false, v.url == element_definition.type.first.profile.first end else - indexed_elements = {} elements.each do |k, v| - v.each_with_index do |vv, kk| - indexed_elements["#{k}[#{kk}]"] = vv if vv.url == element_definition.type.first.profile.first + v.select! do |ext| + ext.url == element_definition.type.first.profile.first end end - elements = indexed_elements end end elements diff --git a/lib/fhir_models/validation/structure_validation_result.rb b/lib/fhir_models/validation/structure_validation_result.rb index ae439a69b..73b6bfdd6 100644 --- a/lib/fhir_models/validation/structure_validation_result.rb +++ b/lib/fhir_models/validation/structure_validation_result.rb @@ -3,6 +3,8 @@ module FHIR module Validation class StructureValidationResult + attr_accessor :validation_hash + def initialize(validation_hash) @validation_hash = validation_hash end diff --git a/lib/fhir_models/validation/structure_validator.rb b/lib/fhir_models/validation/structure_validator.rb index 795004add..b00cdffc6 100644 --- a/lib/fhir_models/validation/structure_validator.rb +++ b/lib/fhir_models/validation/structure_validator.rb @@ -25,7 +25,7 @@ def initialize(profile, use_default_element_validators: true) # @return [Hash] the validation results def validate(resource) validate_against_hierarchy(resource) - FHIR::Validation::StructureValidationResult.new(@snapshot_hierarchy) + FHIR::Validation::StructureValidationResult.new(@snapshot_hierarchy).all_results end # Removes the currently registered element validators @@ -33,11 +33,10 @@ def clear_element_validators @element_validators = Set.new end - # Add Element Validators to the StructureValidator - # - # @param element_validators [Enumerable] - def register_element_validators(element_validators) - @element_validators.merge(Set.new(element_validators)) + # Add additional element validators + # @param element_validator [Enumerable<#validate>] + def register_element_validators(element_validator) + @element_validators.add(*element_validator) end # Add the default Element Validators to the StructureValidator @@ -120,10 +119,6 @@ def add_default_element_validators hierarchy[:slices].values.each { |v| validate_hierarchy(resource, v) } end - def register_element_validator(element_validator) - @element_validators.add(*element_validator) - end - private def blank?(obj) obj.respond_to?(:empty?) ? obj.empty? : obj.nil? end diff --git a/spec/unit/validation/structure_validator_spec.rb b/spec/unit/validation/structure_validator_spec.rb index 5ed0513b4..e966944a8 100644 --- a/spec/unit/validation/structure_validator_spec.rb +++ b/spec/unit/validation/structure_validator_spec.rb @@ -1,44 +1,83 @@ describe FHIR::Validation::StructureValidator do + FIXTURES_DIR = File.join('test', 'fixtures') - let(:profile) {FHIR::StructureDefinition.new} - let(:validator) do - FHIR::Validation::StructureValidator.new(profile) + # let(:profile) {FHIR::StructureDefinition.new} + # let(:validator) do + # FHIR::Validation::StructureValidator.new(profile) + # end + # + let(:profile) {FHIR::StructureDefinition.new(snapshot: {element: elements})} + let(:elements) do + [FHIR::ElementDefinition.new(id: 'Patient', + path: 'Patient', + min: 0, + max: '*'), + FHIR::ElementDefinition.new(id: 'Patient.extension', + path: 'Patient.extension', + min: 0, + max: '*'), + FHIR::ElementDefinition.new(id: 'Patient.extension:foo', + path: 'Patient.extension', + sliceName: 'foo', + min: 0, + max: '*', + type: [ + {code: 'Extension', profile: ['http://foo.org']} + ]), + ] end - context - context 'with US Core Patient Profile Validator' do - let(:us_core_profile_validator) do - us_core_patient = File.join(FIXTURES_DIR, 'us_core', 'StructureDefinition-us-core-patient.json') - json = File.read(us_core_patient) - p_validator = FHIR::Validation::StructureValidator.new(FHIR.from_contents(json)) - #p_validator.register_element_validator(FHIR::Validation::CardinalityValidator) - p_validator - end - - let(:patient_resource) do - patient_record = File.join(FIXTURES_DIR, ['invalid_resources', 'invalid-Patient-example.json']) - json = File.read(patient_record) - FHIR.from_contents(json) - end - - before { validator.register_validator_module(us_core_profile_validator) } + let(:resource) do + FHIR::Patient.new(extension: [{url: 'bar'}, {url: 'foo'}]) + end - it '#validate' do - results = validator.validate(patient_resource) - results.first.all_results - expect(results).to_not be_empty - end + let(:validator) do + FHIR::Validation::StructureValidator.new(profile) + end - it 'checks element cardinality' do - results = validator.validate(patient_resource) - cardinality_results = results.select { |x| x.validation_type == :cardinality } - expect(cardinality_results).to_not be_empty + context 'with the default validators' do + before(:example) do + @results = validator.validate(resource) end - - it 'skips checking the cardinality of the root element' do - results = validator.validate(patient_resource) - root_cardinality_results = results.select { |x| x.element_path == 'Patient' } - expect(root_cardinality_results).to be_empty + it 'returns cardinality results' do + cardinality_results = @results.select { |res| res.validation_type == :cardinality } + expect(cardinality_results.length.positive?).to be_truthy end end + + # context 'with US Core Patient Profile Validator' do + # let(:us_core_profile_validator) do + # us_core_patient = File.join(FIXTURES_DIR, 'us_core', 'StructureDefinition-us-core-patient.json') + # json = File.read(us_core_patient) + # p_validator = FHIR::Validation::StructureValidator.new(FHIR.from_contents(json)) + # #p_validator.register_element_validator(FHIR::Validation::CardinalityValidator) + # p_validator + # end + # + # let(:patient_resource) do + # patient_record = File.join(FIXTURES_DIR, ['invalid_resources', 'invalid-Patient-example.json']) + # json = File.read(patient_record) + # FHIR.from_contents(json) + # end + # + # #before { validator.register_validator_module(us_core_profile_validator) } + # + # it '#validate' do + # results = us_core_profile_validator.validate(patient_resource) + # results.first.all_results + # expect(results).to_not be_empty + # end + # + # it 'checks element cardinality' do + # results = validator.validate(patient_resource) + # cardinality_results = results.select { |x| x.validation_type == :cardinality } + # expect(cardinality_results).to_not be_empty + # end + # + # it 'skips checking the cardinality of the root element' do + # results = validator.validate(patient_resource) + # root_cardinality_results = results.select { |x| x.element_path == 'Patient' } + # expect(root_cardinality_results).to be_empty + # end + # end end \ No newline at end of file diff --git a/spec/unit/validation/validation_api_spec.rb b/spec/unit/validation/validation_api_spec.rb deleted file mode 100644 index 68b6e1215..000000000 --- a/spec/unit/validation/validation_api_spec.rb +++ /dev/null @@ -1,63 +0,0 @@ -describe 'Profile Resource Validation' do - FIXTURES_DIR = File.join('test', 'fixtures') - - let(:validator) {FHIR::Validator.new} - let(:profile_validator) { FHIR::Validation::StructureValidator.new(structure_definition)} - let(:cardinality_validator) { spy(FHIR::Validation::CardinalityValidator)} - let(:element_definition) do - element_definition = FHIR::ElementDefinition.new(id: 'Patient', path: 'Patient') - end - let(:structure_definition) do - FHIR::StructureDefinition.new(snapshot:{element: [element_definition]}) - end - let(:resource) do - {} - end - - it 'initially has no validator modules' do - expect(validator.validator_modules).to be_empty - end - - specify '#register_element_validator' do - profile_validator.register_element_validator(cardinality_validator) - profile_validator.validate(resource) - expect(cardinality_validator).to have_received(:validate) - .with(resource, element_definition) - end - - context 'with US Core Patient Profile Validator' do - let(:us_core_profile_validator) do - us_core_patient = File.join(FIXTURES_DIR, 'us_core', 'StructureDefinition-us-core-patient.json') - json = File.read(us_core_patient) - p_validator = FHIR::Validation::StructureValidator.new(FHIR.from_contents(json)) - p_validator.register_element_validator(FHIR::Validation::CardinalityValidator) - p_validator - end - - let(:patient_resource) do - patient_record = File.join(FIXTURES_DIR, ['invalid_resources', 'invalid-Patient-example.json']) - json = File.read(patient_record) - FHIR.from_contents(json) - end - - before { validator.register_validator_module(us_core_profile_validator) } - - it '#validate' do - results = validator.validate(patient_resource) - results.first.all_results - expect(results).to_not be_empty - end - - it 'checks element cardinality' do - results = validator.validate(patient_resource) - cardinality_results = results.select { |x| x.validation_type == :cardinality } - expect(cardinality_results).to_not be_empty - end - - it 'skips checking the cardinality of the root element' do - results = validator.validate(patient_resource) - root_cardinality_results = results.select { |x| x.element_path == 'Patient' } - expect(root_cardinality_results).to be_empty - end - end -end \ No newline at end of file From c81b750d276c6ef3350601c4e58843a6652c70d3 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Wed, 7 Aug 2019 12:24:57 -0400 Subject: [PATCH 28/80] address retrieval side effect --- lib/fhir_models/validation/retrieval.rb | 4 +- .../validation/structure_validation_result.rb | 5 +-- spec/unit/validation/retrieval_spec.rb | 37 +++++++++++++++++-- .../validation/structure_validator_spec.rb | 2 +- 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/lib/fhir_models/validation/retrieval.rb b/lib/fhir_models/validation/retrieval.rb index 7b7355eb1..0a1db5138 100644 --- a/lib/fhir_models/validation/retrieval.rb +++ b/lib/fhir_models/validation/retrieval.rb @@ -80,11 +80,13 @@ def retrieve_by_element_definition(resource, element_definition, indexed: false, v.url == element_definition.type.first.profile.first end else + sliced_elements = {} elements.each do |k, v| - v.select! do |ext| + sliced_elements[k] = v.select do |ext| ext.url == element_definition.type.first.profile.first end end + elements = sliced_elements end end elements diff --git a/lib/fhir_models/validation/structure_validation_result.rb b/lib/fhir_models/validation/structure_validation_result.rb index 73b6bfdd6..136f27605 100644 --- a/lib/fhir_models/validation/structure_validation_result.rb +++ b/lib/fhir_models/validation/structure_validation_result.rb @@ -1,8 +1,7 @@ -#require_relative 'traverse' module FHIR module Validation + # Represents the results of validating a structure. class StructureValidationResult - attr_accessor :validation_hash def initialize(validation_hash) @@ -48,4 +47,4 @@ def results_for_path(path) end end end -end \ No newline at end of file +end diff --git a/spec/unit/validation/retrieval_spec.rb b/spec/unit/validation/retrieval_spec.rb index b37d224ba..a038bf0d0 100644 --- a/spec/unit/validation/retrieval_spec.rb +++ b/spec/unit/validation/retrieval_spec.rb @@ -2,10 +2,11 @@ let(:retrieval) { FHIR::Validation::Retrieval } let(:resource) do FHIR::Patient.new(id: 2, - name: {given: 'Bob'}, - communication: [{language: 'English'}, {language: 'Spanish'}], - deceasedBoolean: false, - deceasedDateTime: 'YYYY-MM-DD') + extension: [{url: 'bar'}, {url: 'http://foo.org'}], + name: {given: 'Bob'}, + communication: [{language: 'English'}, {language: 'Spanish'}], + deceasedBoolean: false, + deceasedDateTime: 'YYYY-MM-DD') end describe '#retrieve_by_element_definition' do @@ -59,6 +60,34 @@ expect(retrieval.retrieve_by_element_definition(resource, element_definition, normalized: true)).to eq(expected_communications) end end + + context 'with sliced extensions' do + it 'returns all extensions' do + element_definition = FHIR::ElementDefinition.new(id: 'Patient.extension', path: 'Patient.extension') + expect(retrieval.retrieve_by_element_definition(resource, element_definition)).to eq({element_definition.id => resource.extension}) + end + it 'returns the extensions indexed' do + expected_result = {'Patient.extension[0]' => resource.extension[0], + 'Patient.extension[1]' => resource.extension[1]} + element_definition = FHIR::ElementDefinition.new(id: 'Patient.extension', path: 'Patient.extension') + expect(retrieval.retrieve_by_element_definition(resource, element_definition, indexed: true)).to eq(expected_result) + end + it 'returns the sliced extension' do + element_definition = FHIR::ElementDefinition.new(id: 'Patient.extension:foo', + path: 'Patient.extension', + sliceName: 'foo', + type: [{code: 'Extension', profile: ['http://foo.org']}]) + expect(retrieval.retrieve_by_element_definition(resource, element_definition)).to eq({element_definition.path => [resource.extension[1]]}) + end + + it 'returns the sliced extension indexed' do + element_definition = FHIR::ElementDefinition.new(id: 'Patient.extension:foo', + path: 'Patient.extension', + sliceName: 'foo', + type: [{code: 'Extension', profile: ['http://foo.org']}]) + expect(retrieval.retrieve_by_element_definition(resource, element_definition, indexed: true)).to eq({"#{element_definition.path}[1]" => resource.extension[1]}) + end + end end specify '#retrieve_element_with_fhirpath' do diff --git a/spec/unit/validation/structure_validator_spec.rb b/spec/unit/validation/structure_validator_spec.rb index e966944a8..040fa66ce 100644 --- a/spec/unit/validation/structure_validator_spec.rb +++ b/spec/unit/validation/structure_validator_spec.rb @@ -28,7 +28,7 @@ end let(:resource) do - FHIR::Patient.new(extension: [{url: 'bar'}, {url: 'foo'}]) + FHIR::Patient.new(extension: [{url: 'bar'}, {url: 'http://foo.org'}]) end let(:validator) do From 43cefc5ab7d754b87b4b77d3f6433c9ff77e6eba Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Wed, 7 Aug 2019 13:07:54 -0400 Subject: [PATCH 29/80] test terminology validator registration --- .../element_validator/terminology_validator.rb | 6 ++++++ .../terminology_validator_spec.rb | 17 +++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/lib/fhir_models/validation/element_validator/terminology_validator.rb b/lib/fhir_models/validation/element_validator/terminology_validator.rb index b0179cdde..957de6948 100644 --- a/lib/fhir_models/validation/element_validator/terminology_validator.rb +++ b/lib/fhir_models/validation/element_validator/terminology_validator.rb @@ -2,6 +2,7 @@ module FHIR module Validation # Validate Terminology in coded elements class TerminologyValidator + attr_reader :vs_validators def initialize(validators = {}) @vs_validators = validators end @@ -14,6 +15,11 @@ def register_vs_validator(valueset_uri, validator) @vs_validators[valueset_uri] = validator end + # Clear the registered ValueSet and CodeSystem Validators + def clear_vs_validators + @vs_validators = {} + end + # Verify that the element meets terminology requirements # # @param element [Object] The Element of the Resource under test diff --git a/spec/unit/validation/element_validator/terminology_validator_spec.rb b/spec/unit/validation/element_validator/terminology_validator_spec.rb index 4e4ae9a8b..124e9b22c 100644 --- a/spec/unit/validation/element_validator/terminology_validator_spec.rb +++ b/spec/unit/validation/element_validator/terminology_validator_spec.rb @@ -95,6 +95,23 @@ end end + context 'with no code or valueset validators' do + include_context 'code is in the ValueSet' + let(:validator) {FHIR::Validation::TerminologyValidator.new} + it 'contains no validators initially' do + expect(validator.vs_validators).to be_empty + end + + it 'allows additional validators to be added and removed' do + validator.register_vs_validator(value_set_validator.keys.first, value_set_validator[value_set_validator.keys.first]) + expect(validator.vs_validators).to eq(value_set_validator) + + validator.clear_vs_validators + expect(validator.vs_validators).to be_empty + end + + end + describe '#validate' do data_binding_pairs.each do |data_binding_pair| context "with a #{data_binding_pair[0]}, #{data_binding_pair[1]} binding, " do From a1d6059e31a87e280ba57ed192f3456a19fa6016 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Wed, 7 Aug 2019 13:16:57 -0400 Subject: [PATCH 30/80] add tests for terminology checks with a choice of types --- .../terminology_validator_spec.rb | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/spec/unit/validation/element_validator/terminology_validator_spec.rb b/spec/unit/validation/element_validator/terminology_validator_spec.rb index 124e9b22c..6b82c3c6a 100644 --- a/spec/unit/validation/element_validator/terminology_validator_spec.rb +++ b/spec/unit/validation/element_validator/terminology_validator_spec.rb @@ -47,12 +47,14 @@ end end - shared_context 'ElementDefinition' do |binding_strength, type| + shared_context 'ElementDefinition' do |binding_strength, type, second_type = nil| let(:element_definition) do - FHIR::ElementDefinition.new(id: 'Element', - path: 'Element', + types = [{code: type}] + types.push({code: second_type}) if second_type + FHIR::ElementDefinition.new(id: "Element#{'[x]' if second_type}", + path: "Element#{'[x]' if second_type}", binding: {strength: binding_strength, valueSet: value_set}, - type: [{code: type}]) + type: types) end end @@ -113,21 +115,23 @@ end describe '#validate' do - data_binding_pairs.each do |data_binding_pair| - context "with a #{data_binding_pair[0]}, #{data_binding_pair[1]} binding, " do - include_context 'ElementDefinition', data_binding_pair[1], data_binding_pair[0] - include_context 'Resource type', data_binding_pair[0] - validation_combinations.each do |combo| - context "#{'un' unless combo[0]}known ValueSet #{'not ' unless combo[2]}containing the code, and "\ + [nil, 'FakeType'].each do |secondary_type| + data_binding_pairs.each do |data_binding_pair| + context "with a #{data_binding_pair[0]}, #{data_binding_pair[1]} binding, " do + include_context 'ElementDefinition', data_binding_pair[1], data_binding_pair[0], secondary_type + include_context 'Resource type', data_binding_pair[0] + validation_combinations.each do |combo| + context "#{'un' unless combo[0]}known ValueSet #{'not ' unless combo[2]}containing the code, and "\ "#{'un' unless combo[1]}known CodeSystem #{'not ' unless combo[3]}containing the code"do - include_context 'ValueSet is known', combo[0] - include_context 'CodeSystem is known', combo[1] - include_context 'code is in the ValueSet', combo[2] - include_context 'code is in the CodeSystem', combo[3] - before(:example) do - @results = validator.validate(resource, element_definition) + include_context 'ValueSet is known', combo[0] + include_context 'CodeSystem is known', combo[1] + include_context 'code is in the ValueSet', combo[2] + include_context 'code is in the CodeSystem', combo[3] + before(:example) do + @results = validator.validate(resource, element_definition) + end + include_examples 'expected results', combo, data_binding_pair[1] end - include_examples 'expected results', combo, data_binding_pair[1] end end end From 2a196143938d516d4ec17937a409ddb8e98ed903 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Wed, 7 Aug 2019 14:13:10 -0400 Subject: [PATCH 31/80] add tests and remove some unused functions --- .../validation/structure_validation_result.rb | 26 ------------ lib/fhir_models/validation/validator.rb | 42 ------------------- .../terminology_validator_spec.rb | 39 +++++++++++++++++ .../validation/structure_validator_spec.rb | 36 ---------------- 4 files changed, 39 insertions(+), 104 deletions(-) delete mode 100644 lib/fhir_models/validation/validator.rb diff --git a/lib/fhir_models/validation/structure_validation_result.rb b/lib/fhir_models/validation/structure_validation_result.rb index 136f27605..583310dcc 100644 --- a/lib/fhir_models/validation/structure_validation_result.rb +++ b/lib/fhir_models/validation/structure_validation_result.rb @@ -8,36 +8,10 @@ def initialize(validation_hash) @validation_hash = validation_hash end - def traverse_results(&block) - traverse(@validation_hash, :results, &block) - end - - def traverse_element_definition(&block) - traverse(@validation_hash, :element_definition, &block) - end - def all_results all_validation_results(@validation_hash) end - def results_for_path(path) - slices = path.split(':') - path = slices.shift.splt('.') - hierarchy = @validation_hash[path.shift.to_sym] - path.inject(hierarchy) do |memo, k| - memo[:path][k.to_sym] - end[:results] - end - - private def traverse(hierarchy, desired_key, &block) - hierarchy.each do |_, v| - # yield the desired value - yield(v[:desired_key]) - traverse(v[:slices], desired_key, &block) unless v[:slices].empty? - traverse(v[:path], desired_key, &block) unless v[:path].empty? - end - end - private def all_validation_results(hierarchy) hierarchy.flat_map do |_, v| slice_results = v[:slices].empty? ? [] : all_validation_results(v[:slices]) diff --git a/lib/fhir_models/validation/validator.rb b/lib/fhir_models/validation/validator.rb deleted file mode 100644 index 17fd750af..000000000 --- a/lib/fhir_models/validation/validator.rb +++ /dev/null @@ -1,42 +0,0 @@ -module FHIR - # FHIR Resource Validator - class Validator - attr_reader :validator_modules - attr_reader :show_skipped - - # Creates a FHIR Validator - # - # @param validator_modules[ValidatorModule, Array, #validate] An array of validator_modules - def initialize(validator_modules = []) - @validator_modules = Set.new - add_validator_module(validator_modules) - end - - # Register a validator_module - # @param [#validate] validator_module - def register_validator_module(validator_module) - @validator_modules.each { |validator| validator.show_skipped = @show_skipped if validator.respond_to?(:show_skipped) } - add_validator_module(validator_module) - end - - # @param resource [FHIR::Model] The Resource to be validated - # @return [Hash] the validation results - def validate(resource) - @validator_modules.flat_map { |validator| validator.validate(resource) } - end - - def show_skipped=(skip) - @show_skipped = skip - @validator_modules.each { |validator| validator.show_skipped = skip } - end - - # Helper method for adding validator modules - # - # This allows an individual validator to be passed or an array of validators - # - # @param validators [#validate] The validators to be added - private def add_validator_module(validators) - @validator_modules.merge([validators].flatten) - end - end -end diff --git a/spec/unit/validation/element_validator/terminology_validator_spec.rb b/spec/unit/validation/element_validator/terminology_validator_spec.rb index 6b82c3c6a..eeec9ae35 100644 --- a/spec/unit/validation/element_validator/terminology_validator_spec.rb +++ b/spec/unit/validation/element_validator/terminology_validator_spec.rb @@ -112,6 +112,45 @@ expect(validator.vs_validators).to be_empty end + it 'warns if the code is missing' do + results = validator.validate(FHIR::CodeableConcept.new(coding: {system: 'adsf'}), + FHIR::ElementDefinition.new(id: 'Element', + path: 'Element', + binding: {strength: 'required', valueSet: 'asdf'}, + type: [{code: 'CodeableConcept'}])) + expect(results.size).to eq(1) + expect(results).to all(have_attributes(validation_type: :terminology)) + expect(results).to all(have_attributes(:text => a_string_including('missing code'))) + expect(results).to all(have_attributes(result: :warn)) + + end + + it 'warns if the system is missing' do + results = validator.validate(FHIR::CodeableConcept.new(coding: {code: 'waldo'}), + FHIR::ElementDefinition.new(id: 'Element', + path: 'Element', + binding: {strength: 'required', valueSet: 'asdf'}, + type: [{code: 'CodeableConcept'}])) + + expect(results.size).to eq(1) + expect(results).to all(have_attributes(validation_type: :terminology)) + expect(results).to all(have_attributes(:text => a_string_including('missing system'))) + expect(results).to all(have_attributes(result: :warn)) + end + + it 'warns if the code and system are missing' do + results = validator.validate(FHIR::CodeableConcept.new(coding: {text: 'nope'}), + FHIR::ElementDefinition.new(id: 'Element', + path: 'Element', + binding: {strength: 'required', valueSet: 'asdf'}, + type: [{code: 'CodeableConcept'}])) + + expect(results.size).to eq(2) + expect(results).to all(have_attributes(validation_type: :terminology)) + expect(results).to all(have_attributes(:text => a_string_including('missing'))) + expect(results).to all(have_attributes(result: :warn)) + end + end describe '#validate' do diff --git a/spec/unit/validation/structure_validator_spec.rb b/spec/unit/validation/structure_validator_spec.rb index 040fa66ce..3e7964fe3 100644 --- a/spec/unit/validation/structure_validator_spec.rb +++ b/spec/unit/validation/structure_validator_spec.rb @@ -44,40 +44,4 @@ expect(cardinality_results.length.positive?).to be_truthy end end - - # context 'with US Core Patient Profile Validator' do - # let(:us_core_profile_validator) do - # us_core_patient = File.join(FIXTURES_DIR, 'us_core', 'StructureDefinition-us-core-patient.json') - # json = File.read(us_core_patient) - # p_validator = FHIR::Validation::StructureValidator.new(FHIR.from_contents(json)) - # #p_validator.register_element_validator(FHIR::Validation::CardinalityValidator) - # p_validator - # end - # - # let(:patient_resource) do - # patient_record = File.join(FIXTURES_DIR, ['invalid_resources', 'invalid-Patient-example.json']) - # json = File.read(patient_record) - # FHIR.from_contents(json) - # end - # - # #before { validator.register_validator_module(us_core_profile_validator) } - # - # it '#validate' do - # results = us_core_profile_validator.validate(patient_resource) - # results.first.all_results - # expect(results).to_not be_empty - # end - # - # it 'checks element cardinality' do - # results = validator.validate(patient_resource) - # cardinality_results = results.select { |x| x.validation_type == :cardinality } - # expect(cardinality_results).to_not be_empty - # end - # - # it 'skips checking the cardinality of the root element' do - # results = validator.validate(patient_resource) - # root_cardinality_results = results.select { |x| x.element_path == 'Patient' } - # expect(root_cardinality_results).to be_empty - # end - # end end \ No newline at end of file From 891d441c202313e5e70e3a12dc036c100a89300f Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Thu, 8 Aug 2019 09:09:55 -0400 Subject: [PATCH 32/80] FHIR::Models use FHIR::Validation. fixes and updates for local_name attributes --- lib/fhir_models/bootstrap/model.rb | 193 +----------------- .../element_validator/data_type_validator.rb | 14 +- lib/fhir_models/validation/retrieval.rb | 11 +- .../data_type_validator_spec.rb | 8 +- test/unit/json_validation_test.rb | 9 +- test/unit/profile_validation_test.rb | 2 +- test/unit/xml_validation_test.rb | 10 +- 7 files changed, 42 insertions(+), 205 deletions(-) diff --git a/lib/fhir_models/bootstrap/model.rb b/lib/fhir_models/bootstrap/model.rb index 16edf7b3c..69b32a2b5 100644 --- a/lib/fhir_models/bootstrap/model.rb +++ b/lib/fhir_models/bootstrap/model.rb @@ -102,173 +102,14 @@ def compare_attribute(left, right, exclude = []) end def valid? - validate.empty? + validate.select { |res| res.result == :fail }.empty? end deprecate :is_valid?, :valid? - def validate(contained = nil) - validate_profile(self.class::METADATA, contained) - end - - def validate_profile(metadata, contained = nil) - contained_here = [instance_variable_get('@contained'.to_sym)].flatten - contained_here << contained - contained_here = contained_here.flatten.compact - errors = {} - metadata.each do |field, meta| - if meta.is_a?(Array) - # this field has been 'sliced' - meta.each do |slice| - local_name = slice['local_name'] || field - value = [instance_variable_get("@#{local_name}".to_sym)].flatten.compact - subset = [] # subset is the values associated with just this slice - if slice['type'] == 'Extension' - subset = if slice['type_profiles'] - value.select { |x| slice['type_profiles'].include?(x.url) } - else - value - end - else - FHIR.logger.warn 'Validation not supported on slices (except for Extensions)' - end - validate_field(field, subset, contained_here, slice, errors) - end - else - local_name = meta['local_name'] || field - value = [instance_variable_get("@#{local_name}".to_sym)].flatten.compact - validate_field(field, value, contained_here, meta, errors) - end - end # metadata.each - # check multiple types - multiple_types = begin - self.class::MULTIPLE_TYPES - rescue - {} - end - multiple_types.each do |prefix, suffixes| - present = [] - suffixes.each do |suffix| - typename = "#{prefix}#{suffix[0].upcase}#{suffix[1..-1]}" - # check which multiple data types are actually present, not just errors - # actually, this might be allowed depending on cardinality - value = instance_variable_get("@#{typename}") - present << typename if !value.nil? || (value.is_a?(Array) && !value.empty?) - end - errors[prefix] = ["#{prefix}[x]: more than one type present."] if present.length > 1 - # remove errors for suffixes that are not present - next unless present.length == 1 - suffixes.each do |suffix| - typename = "#{prefix}#{suffix[0].upcase}#{suffix[1..-1]}" - errors.delete(typename) unless present.include?(typename) - end - end - errors.keep_if { |_k, v| (v && !v.empty?) } - end - - # ----- validate a field ----- - # field: the field name - # value: an array of values for this field - # contained_here: all contained resources to be considered - # meta: the metadata definition for this field (or slice) - # errors: the ongoing list of errors - def validate_field(field, value, contained_here, meta, errors) - errors[field] = [] - # check cardinality - count = value.length - unless count >= meta['min'] && count <= meta['max'] - errors[field] << "#{meta['path']}: invalid cardinality. Found #{count} expected #{meta['min']}..#{meta['max']}" - end - # check datatype - datatype = meta['type'] - value.each do |v| - klassname = v.class.name.gsub('FHIR::', '') - # if the data type is a generic Resource, validate it - if datatype == 'Resource' - if FHIR::RESOURCES.include?(klassname) - validation = v.validate(contained_here) - errors[field] << validation unless validation.empty? - else - errors[field] << "#{meta['path']}: expected Resource, found #{klassname}" - end - # if the data type is a Reference, validate it, but also check the - # type_profiles metadata. For example, if it should be a Reference(Patient) - elsif datatype == 'Reference' - if klassname == 'Reference' - validation = v.validate(contained_here) - errors[field] << validation unless validation.empty? - validate_reference_type(v, meta, contained_here, errors[field]) - else - errors[field] << "#{meta['path']}: expected Reference, found #{klassname}" - end - # if the data type is a particular resource or complex type or BackBone element within this resource - elsif FHIR::RESOURCES.include?(datatype) || FHIR::TYPES.include?(datatype) || v.class.name.start_with?(self.class.name) - if datatype == klassname - validation = v.validate(contained_here) - errors[field] << validation unless validation.empty? - else - errors[field] << "#{meta['path']}: incorrect type. Found #{klassname} expected #{datatype}" - end - # if the data type is a primitive, test the regular expression (if any) - elsif FHIR::PRIMITIVES.include?(datatype) - primitive_meta = FHIR::PRIMITIVES[datatype] - if primitive_meta['regex'] && primitive_meta['type'] != 'number' - match = (v.to_s =~ Regexp.new(primitive_meta['regex'])) - errors[field] << "#{meta['path']}: #{v} does not match #{datatype} regex" if match.nil? - else - errors[field] << "#{meta['path']}: #{v} is not a valid #{datatype}" unless FHIR.primitive?(datatype: datatype, value: v) - end - end - # check binding - next unless meta['binding'] - next unless meta['binding']['strength'] == 'required' - the_codes = [v] - if meta['type'] == 'Coding' - the_codes = [v.code] - elsif meta['type'] == 'CodeableConcept' - the_codes = v.coding.map(&:code).compact - end - has_valid_code = false - if meta['valid_codes'] - meta['valid_codes'].each do |_key, codes| - has_valid_code = true unless (codes & the_codes).empty? - break if has_valid_code - end - else - the_codes.each do |code| - has_valid_code = true if check_binding_uri(meta['binding']['uri'], code) - break if has_valid_code - end - end - errors[field] << "#{meta['path']}: invalid codes #{the_codes}" unless has_valid_code - end # value.each - errors.delete(field) if errors[field].empty? - end - - def validate_reference_type(ref, meta, contained_here, errors) - return unless ref.reference && meta['type_profiles'] - return if ref.reference.start_with?('urn:uuid:', 'urn:oid:') - matches_one_profile = false - meta['type_profiles'].each do |p| - basetype = p.split('/').last - matches_one_profile = true if ref.reference.include?(basetype) - # check profiled resources - profile_basetype = FHIR::Definitions.basetype(p) - matches_one_profile = true if profile_basetype && ref.reference.include?(profile_basetype) - end - matches_one_profile = true if meta['type_profiles'].include?('http://hl7.org/fhir/StructureDefinition/Resource') - if !matches_one_profile && ref.reference.start_with?('#') - # we need to look at the local contained resources - r = contained_here.find { |x| x.id == ref.reference[1..-1] } - if !r.nil? - meta['type_profiles'].each do |p| - p = p.split('/').last - matches_one_profile = true if r.resourceType == p - end - else - FHIR.logger.warn "Unable to resolve reference #{ref.reference}" - end - end - errors << "#{meta['path']}: incorrect Reference type, expected #{meta['type_profiles'].map { |x| x.split('/').last }.join('|')}" unless matches_one_profile + def validate + type_name = self.class.name.split('::').last + type_def = FHIR::Definitions.type_definition(type_name) || FHIR::Definitions.resource_definition(type_name) + FHIR::Validation::StructureValidator.new(type_def).validate(self) end def primitive?(datatype, value) @@ -276,29 +117,5 @@ def primitive?(datatype, value) FHIR.primitive?(datatype: datatype, value: value) end deprecate :is_primitive?, :primitive? - - def check_binding_uri(uri, value) - valid = false - uri = uri[0..-7] if uri.end_with?('|4.0.0') - valueset = FHIR::Definitions.get_codes(uri) - - if uri == 'http://hl7.org/fhir/ValueSet/mimetypes' || uri == 'http://www.rfc-editor.org/bcp/bcp13.txt' - matches = MIME::Types[value] - json_or_xml = value.downcase.include?('xml') || value.downcase.include?('json') - known_weird = ['text/cql', 'application/cql+text', 'application/hl7-v2'].include?(value) - valid = json_or_xml || known_weird || (!matches.nil? && !matches.empty?) - elsif uri == 'http://hl7.org/fhir/ValueSet/languages' || 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?) - elsif valueset.nil? - FHIR.logger.warn "Unable to check_binding_uri on unknown ValueSet: #{uri}" - else - valid = valueset.values.flatten.include?(value) - end - - valid - end - - private :validate_reference_type, :check_binding_uri, :validate_field end end diff --git a/lib/fhir_models/validation/element_validator/data_type_validator.rb b/lib/fhir_models/validation/element_validator/data_type_validator.rb index e2db21136..35d7765d0 100644 --- a/lib/fhir_models/validation/element_validator/data_type_validator.rb +++ b/lib/fhir_models/validation/element_validator/data_type_validator.rb @@ -34,7 +34,7 @@ def self.validate_element(element, element_definition, path) element_definition.type.find do |datatype| cap_code = "#{datatype.code[0].capitalize}#{datatype.code[1..-1]}" - /[^.]+$/.match(element_definition.path.gsub('[x]', cap_code)) == /[^.]+$/.match(path) + /[^.]+$/.match(element_definition.path.gsub('[x]', cap_code)).to_s == /[^.]+$/.match(path).to_s end.code end type_def = FHIR::Definitions.type_definition(type_code) || FHIR::Definitions.resource_definition(type_code) @@ -44,7 +44,17 @@ def self.validate_element(element, element_definition, path) return FHIR::ValidationResult.new(element_definition: element_definition, validation_type: :datatype, result: :warn, - text: "Unknown type: #{type_code}", + text: (if type_code.nil? + 'Type code not provided or extends the primitive code' + else + "Unknown type: #{type_code}" + end), + element_path: path || element_definition.path) + elsif type_def.kind == 'primitive-type' + return FHIR::ValidationResult.new(element_definition: element_definition, + validation_type: :datatype, + result: :warn, + text: 'Cannot validate primitive-type as a Structure', element_path: path || element_definition.path) end type_validator = FHIR::Validation::StructureValidator.new(type_def) diff --git a/lib/fhir_models/validation/retrieval.rb b/lib/fhir_models/validation/retrieval.rb index 0a1db5138..a5f72eb8a 100644 --- a/lib/fhir_models/validation/retrieval.rb +++ b/lib/fhir_models/validation/retrieval.rb @@ -16,7 +16,8 @@ def retrieve_element_with_fhirpath(path, resource, indexed = true) desired_elements = spath.inject(fhirpath_elements) do |memo, meth| digging = {} memo.each do |k, v| - elms = v.send(meth) if v.is_a? FHIR::Model # FHIR Primitives are not modeled and will throw NoMethod Error + fix_name = %w[class method resourceType].include?(meth) ? "local_#{meth}" : meth + elms = v.send(fix_name) if v.is_a? FHIR::Model # FHIR Primitives are not modeled and will throw NoMethod Error # More than one element where the FHIRPath needs indexing if elms.respond_to? :each_with_index elms.each_with_index do |vv, kk| @@ -35,7 +36,8 @@ def retrieve_element_with_fhirpath(path, resource, indexed = true) # If we don't want to index the last element (useful for testing cardinality) not_indexed = {} desired_elements.each do |k, v| - elms = v.send(last) if v.is_a? FHIR::Model # FHIR Primitives are not modeled and will throw NoMethod Error + fix_name = %w[class method resourceType].include?(last) ? "local_#{last}" : last + elms = v.send(fix_name) if v.is_a? FHIR::Model # FHIR Primitives are not modeled and will throw NoMethod Error not_indexed["#{k}.#{last}"] = elms end not_indexed @@ -54,11 +56,12 @@ def retrieve_by_element_definition(resource, element_definition, indexed: false, elements.merge!(type_element) unless blank?(type_element) end if normalized - choice_type_elements = Hash.new([]) + choice_type_elements = {} elements.each do |k, v| renorm = k.rpartition('.').first normalized_path = "#{renorm}.#{element_definition.path.split('.').last}" - choice_type_elements[normalized_path] = choice_type_elements[normalized_path].push(v) + choice_type_elements[normalized_path] ||= [] + choice_type_elements[normalized_path].push(v).compact! end elements = choice_type_elements end diff --git a/spec/unit/validation/element_validator/data_type_validator_spec.rb b/spec/unit/validation/element_validator/data_type_validator_spec.rb index 0c3bdd68f..d28324aa1 100644 --- a/spec/unit/validation/element_validator/data_type_validator_spec.rb +++ b/spec/unit/validation/element_validator/data_type_validator_spec.rb @@ -29,8 +29,8 @@ it 'returns an array of results for the single type' do results = validator.validate(resource, element_definition) expect(results).to_not be_empty - expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(result: :pass)) + expect(results.select {|res| res.result == :fail}).to be_empty + expect(results.select {|res| res.validation_type == :cardinality}).to_not be_empty end it 'Returns a warning if the type is unknown' do element_definition.type.first.code = 'Foo' @@ -61,10 +61,10 @@ it 'returns an array of results for elements with a choice of type' do results = validator.validate(resource, element_definition) expect(results).to_not be_empty - expect(results).to all(have_attributes(validation_type: :cardinality)) expect(results).to include(have_attributes(element_path: a_string_including(element_definition.type[0].code))) expect(results).to include(have_attributes(element_path: a_string_including(element_definition.type[1].code))) - expect(results).to all(have_attributes(result: :pass)) + expect(results.select {|res| res.result == :fail}).to be_empty + expect(results.select {|res| res.validation_type == :cardinality}).to_not be_empty end end end diff --git a/test/unit/json_validation_test.rb b/test/unit/json_validation_test.rb index dbecc6023..3545434cf 100644 --- a/test/unit/json_validation_test.rb +++ b/test/unit/json_validation_test.rb @@ -22,10 +22,13 @@ class JsonValidationTest < Test::Unit::TestCase def run_json_validation_test(example_file, example_name) input_json = File.read(example_file) resource = FHIR::Json.from_json(input_json) - errors = resource.validate + errors = resource.validate.select {|res| res.result == :fail} unless errors.empty? - File.open("#{ERROR_DIR}/#{example_name}.err", 'w:UTF-8') { |file| file.write(JSON.pretty_unparse(errors)) } - File.open("#{ERROR_DIR}/#{example_name}.json", 'w:UTF-8') { |file| file.write(input_json) } + File.open("#{ERROR_DIR}/#{example_name}.err", 'w:UTF-8') do |file| + errors.each do |error| + file.write("Validation Type: #{error.validation_type}. Element Path: #{error.element_path}. Message: #{error.text} \n") + end + end end assert errors.empty?, "Resource failed to validate: #{errors}" # check memory diff --git a/test/unit/profile_validation_test.rb b/test/unit/profile_validation_test.rb index 418ac976d..d50e6f203 100644 --- a/test/unit/profile_validation_test.rb +++ b/test/unit/profile_validation_test.rb @@ -38,7 +38,7 @@ def run_profile_validation(example_file, example_name) errors << "Validated against #{resource.meta.profile.first}" unless errors.empty? else # validate the base resource - errors = resource.validate + errors = resource.validate.select {|res| res.result == :fail} errors << "Validated against base resource definition" unless errors.empty? end unless errors.empty? diff --git a/test/unit/xml_validation_test.rb b/test/unit/xml_validation_test.rb index d3be5905d..4e2673202 100644 --- a/test/unit/xml_validation_test.rb +++ b/test/unit/xml_validation_test.rb @@ -23,10 +23,14 @@ def run_xml_validation_test(example_file, example_name) omit 'fhir_models does not support primitive extensions' if PRIMITIVE_EXTENSIONS.include?(example_name) input_xml = File.read(example_file) resource = FHIR::Xml.from_xml(input_xml) - errors = resource.validate + errors = resource.validate.select {|res| res.result == :fail} + unless errors.empty? - File.open("#{ERROR_DIR}/#{example_name}.err", 'w:UTF-8') { |file| file.write(JSON.pretty_unparse(errors)) } - File.open("#{ERROR_DIR}/#{example_name}.xml", 'w:UTF-8') { |file| file.write(input_xml) } + File.open("#{ERROR_DIR}/#{example_name}.err", 'w:UTF-8') do |file| + errors.each do |error| + file.write("Validation Type: #{error.validation_type}. Element Path: #{error.element_path}. Message: #{error.text} \n") + end + end end assert errors.empty?, 'Resource failed to validate.' # check memory From da7dc2e832dcb937c283ff65c9e466b0da452ca4 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Thu, 8 Aug 2019 11:28:09 -0400 Subject: [PATCH 33/80] StructureDefinition Validation use FHIR::Validator --- .../fhir_ext/structure_definition.rb | 418 +----------------- lib/fhir_models/validation/retrieval.rb | 5 +- .../validation/structure_validator.rb | 3 + test/unit/profile_validation_test.rb | 312 +------------ 4 files changed, 8 insertions(+), 730 deletions(-) diff --git a/lib/fhir_models/fhir_ext/structure_definition.rb b/lib/fhir_models/fhir_ext/structure_definition.rb index ef58d9416..2b1433ddc 100644 --- a/lib/fhir_models/fhir_ext/structure_definition.rb +++ b/lib/fhir_models/fhir_ext/structure_definition.rb @@ -15,426 +15,12 @@ class StructureDefinition # Profile Validation # ------------------------------------------------------------------------- - class << self; attr_accessor :vs_validators end - @vs_validators = {} - def self.validates_vs(valueset_uri, &validator_fn) - @vs_validators[valueset_uri] = validator_fn - end - - def self.clear_validates_vs(valueset_uri) - @vs_validators.delete valueset_uri - end - - def self.clear_all_validates_vs - @vs_validators = {} - end - def validates_resource?(resource) - validate_resource(resource).empty? + validate_resource(resource).select { |res| res.result == :fail }.empty? end 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 - - def validates_hash?(hash) - @errors = [] - @warnings = [] - valid_json?(hash) if hash - @errors - end - - # Checks whether or not the "json" is valid according to this definition. - # json == the raw json for a FHIR resource - 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 - deprecate :is_valid_json?, :valid_json? - - 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 - - def describe_element(element) - if element.path.end_with?('.extension', '.modifierExtension') && element.sliceName - "#{element.path} (#{element.sliceName})" - else - element.path - end - end - - 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 - - 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) - 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::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 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.valueSet - if valueset_uri.include?('|') - x = valueset_uri.index('|') - valueset_uri = valueset_uri[0..x - 1] - end - vcc = FHIR::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| - begin - result = FluentPath.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 - 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 - - 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 - - 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 - element.fixed + FHIR::Validation::StructureValidator.new(self).validate(resource) end - - # data_type_code == a FHIR DataType code (see http://hl7.org/fhir/2015May/datatypes.html) - # value == the representation of the value - 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 - deprecate :is_data_type?, :data_type? - - 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 - - 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 - deprecate :is_some_type_of_xml_or_json, :some_type_of_xml_or_json? - - private :valid_json?, :get_json_nodes, :build_hierarchy, :verify_element, :check_binding_element end end diff --git a/lib/fhir_models/validation/retrieval.rb b/lib/fhir_models/validation/retrieval.rb index a5f72eb8a..b6c034f37 100644 --- a/lib/fhir_models/validation/retrieval.rb +++ b/lib/fhir_models/validation/retrieval.rb @@ -71,10 +71,9 @@ def retrieve_by_element_definition(resource, element_definition, indexed: false, # Handle Slices if element_definition.sliceName + # TODO: Need to be able to get other types of slices # Grab Extension slices - raise UnhandledSlice("Slice has more than one type. #{element_definition.id}") unless element_definition.type.one? - - raise UnhandledSlice("Slice type #{element_definition.type.code} is not handled. Only Extension slices are handled") unless element_definition.type.first.code == 'Extension' + return {} unless element_definition.type.first.code == 'Extension' # Only select the elements which match the slice profile. if indexed diff --git a/lib/fhir_models/validation/structure_validator.rb b/lib/fhir_models/validation/structure_validator.rb index b00cdffc6..f868f5581 100644 --- a/lib/fhir_models/validation/structure_validator.rb +++ b/lib/fhir_models/validation/structure_validator.rb @@ -24,7 +24,10 @@ def initialize(profile, use_default_element_validators: true) # @param resource [FHIR::Model] The Resource to be validated # @return [Hash] the validation results def validate(resource) + return [FHIR::ValidationResult.new(validation_type: :structure, result: :fail)] unless resource.is_a? FHIR::Model + validate_against_hierarchy(resource) + FHIR::Validation::StructureValidationResult.new(@snapshot_hierarchy).all_results end diff --git a/test/unit/profile_validation_test.rb b/test/unit/profile_validation_test.rb index d50e6f203..f14886a57 100644 --- a/test/unit/profile_validation_test.rb +++ b/test/unit/profile_validation_test.rb @@ -34,7 +34,7 @@ def run_profile_validation(example_file, example_name) profile = PROFILES[resource.meta.profile.first] profile = FHIR::Definitions.profile(resource.meta.profile.first) unless profile assert profile, "Failed to find profile: #{resource.meta.profile.first}" - errors = profile.validate_resource(resource) + errors = profile.validate_resource(resource).select { |res| res.result == :fail } errors << "Validated against #{resource.meta.profile.first}" unless errors.empty? else # validate the base resource @@ -89,314 +89,4 @@ def test_profile_with_multiple_extensions after = check_memory assert_memory(before, after) end - - def test_language_binding_validation - binding_strength = FHIR::Resource::METADATA['language']['binding']['strength'] - FHIR::Resource::METADATA['language']['binding']['strength'] = 'required' - model = FHIR::Resource.new('language' => 'en-US') - assert model.valid?, 'Language validation failed.' - FHIR::Resource::METADATA['language']['binding']['strength'] = binding_strength - # check memory - before = check_memory - model = nil - wait_for_gc - after = check_memory - assert_memory(before, after) - end - - def test_cardinality_check - sd = FHIR::StructureDefinition.new - - element = FHIR::ElementDefinition.new('min' => 0, 'max' => '1', 'path' => "test1") - nodes = [] - sd.errors = [] - sd.send(:verify_cardinality, element, nodes) - assert_empty(sd.errors) - - nodes = ["one"] - sd.errors = [] - sd.send(:verify_cardinality, element, nodes) - assert_empty(sd.errors) - - nodes = [1, 2, 3] - sd.errors = [] - sd.verify_cardinality(element, nodes) - assert_equal("test1 failed cardinality test (0..1) -- found 3", sd.errors[0]) - - element = FHIR::ElementDefinition.new('min' => 1, 'max' => '1', 'path' => "test2") - nodes = [] - sd.errors = [] - sd.send(:verify_cardinality, element, nodes) - assert_equal("test2 failed cardinality test (1..1) -- found 0", sd.errors[0]) - - nodes = ["one"] - sd.errors = [] - sd.send(:verify_cardinality, element, nodes) - assert_empty(sd.errors) - - nodes = [1, 2, 3] - sd.errors = [] - sd.send(:verify_cardinality, element, nodes) - assert_equal("test2 failed cardinality test (1..1) -- found 3", sd.errors[0]) - - element = FHIR::ElementDefinition.new('min' => 2, 'max' => '*', 'path' => "test3") - nodes = [] - sd.errors = [] - sd.send(:verify_cardinality, element, nodes) - assert_equal("test3 failed cardinality test (2..Infinity) -- found 0", sd.errors[0]) - - nodes = ["one"] - sd.errors = [] - sd.send(:verify_cardinality, element, nodes) - assert_equal("test3 failed cardinality test (2..Infinity) -- found 1", sd.errors[0]) - - nodes = [1, 2, 3] - sd.errors = [] - sd.send(:verify_cardinality, element, nodes) - assert_empty(sd.errors) - end - - def test_maximum_string_length_check - sd = FHIR::StructureDefinition.new - sd.warnings = [] - - element = FHIR::ElementDefinition.new('path' => 'string', 'type' => [{ 'code' => 'String' }], 'maxLength' => 4, 'min' => 0, 'max' => '*') - sd.hierarchy = OpenStruct.new(path: 'x') # just a hack to make this work, wish it was cleaner - sd.errors = [] - sd.send(:verify_element, element, 'string' => "1234") - assert_empty(sd.errors) - - sd.errors = [] - sd.send(:verify_element, element, 'string' => "12345") - assert_equal("string exceed maximum length of 4: 12345", sd.errors[0]) - - element = FHIR::ElementDefinition.new('path' => 'string', 'type' => [{ 'code' => 'String' }], 'min' => 0, 'max' => '*') # no maxlength - - sd.errors = [] - sd.send(:verify_element, element, 'string' => "1234") - assert_empty(sd.errors) - - sd.errors = [] - long_string = (1..10000).map{ rand(10000).to_s }.join(', ') # somewhere in the range of 60k chars - sd.send(:verify_element, element, 'string' => long_string) - assert_empty(sd.errors) - end - - def test_fixed_value - sd = FHIR::StructureDefinition.new - - element = FHIR::ElementDefinition.new('path' => "fixed_value_test") # fixed == nil - sd.errors = [] - sd.verify_fixed_value(element, nil) - assert_empty(sd.errors) - - sd.errors = [] - sd.verify_fixed_value(element, "some_other_value_it_doesnt_matter") - assert_empty(sd.errors) - - element = FHIR::ElementDefinition.new('path' => "fixed_value_test", 'fixedString' => "string_value") - sd.errors = [] - sd.verify_fixed_value(element, nil) - assert_equal("fixed_value_test value of '' did not match fixed value: string_value", sd.errors[0]) - - sd.errors = [] - sd.verify_fixed_value(element, "string_value") - assert_empty(sd.errors) - - sd.errors = [] - sd.verify_fixed_value(element, "some_other_value") - assert_equal("fixed_value_test value of 'some_other_value' did not match fixed value: string_value", sd.errors[0]) - - element = FHIR::ElementDefinition.new('path' => "fixed_value_test", 'fixedCodeableConcept' => { 'coding' => [{ 'system' => 'http://ncimeta.nci.nih.gov', 'code' => 'C2004062' }] } ) - sd.errors = [] - sd.verify_fixed_value(element, nil) - assert_equal("fixed_value_test value of '' did not match fixed value: #{element.fixed}", sd.errors[0]) - # these test cases use the string interpolation in because the error message includes the object ID which isn't constant - - sd.errors = [] - sd.verify_fixed_value(element, "some_other_value") - assert_equal("fixed_value_test value of 'some_other_value' did not match fixed value: #{element.fixed}", sd.errors[0]) - - sd.errors = [] - sd.verify_fixed_value(element, FHIR::CodeableConcept.new('coding' => [{ 'system' => 'http://ncimeta.nci.nih.gov', 'code' => 'C2004062' }])) - assert_empty(sd.errors) - - sd.errors = [] - value = FHIR::CodeableConcept.new('coding' => [{ 'system' => 'http://snomed.info/sct', 'code' => 'C2004062' }]) - sd.verify_fixed_value(element, value) - assert_equal("fixed_value_test value of '#{value}' did not match fixed value: #{element.fixed}", sd.errors[0]) - end - - def test_codeableConcept_pattern - sd = FHIR::StructureDefinition.new - sd.warnings = [] - - element = FHIR::ElementDefinition.new('path' => 'cc', 'type' => [{ 'code' => 'CodeableConcept' }], 'min' => 1, 'max' => '1', - 'patternCodeableConcept' => { 'coding' => [{ 'system' => 'http://ncimeta.nci.nih.gov', 'code' => 'C2004062' }] }) - sd.hierarchy = OpenStruct.new(path: 'x') # just a hack to make this work, wish it was cleaner - sd.errors = [] - sd.send(:verify_element, element, 'cc' => { 'coding' => [{ 'system' => 'http://ncimeta.nci.nih.gov', 'code' => 'C2004062' }] }) # exact match - assert_empty(sd.errors) - - sd.errors = [] - sd.send(:verify_element, element, 'cc' => { 'coding' => [{ 'system' => 'http://ncimeta.nci.nih.gov', 'code' => 'C2004062' }], 'text' => 'some dummy text' }) # added text - assert_empty(sd.errors) - - sd.errors = [] - sd.send(:verify_element, element, 'cc' => { 'coding' => [{ 'system' => 'http://ncimeta.nci.nih.gov', 'code' => 'C2004062', 'display' => 'some_value' }] }) # match, with added 'display' - assert_empty(sd.errors) - - sd.errors = [] - sd.send(:verify_element, element, 'cc' => { 'coding' => [{ 'system' => 'http://ncimeta.nci.nih.gov', 'code' => 'C2222222' }] }) # wrong code - assert_equal("cc CodeableConcept did not match defined pattern: {\"coding\"=>[{\"system\"=>\"http://ncimeta.nci.nih.gov\", \"code\"=>\"C2004062\"}]}", sd.errors[0]) - - sd.errors = [] - sd.send(:verify_element, element, 'cc' => { 'coding' => [{ 'system' => 'http://hl7.org/fhir/sid/icd-10', 'code' => 'Q841' }] }) # completely different - assert_equal("cc CodeableConcept did not match defined pattern: {\"coding\"=>[{\"system\"=>\"http://ncimeta.nci.nih.gov\", \"code\"=>\"C2004062\"}]}", sd.errors[0]) - end - - def test_invalid_value_per_type - sd = FHIR::StructureDefinition.new - sd.warnings = [] - sd.hierarchy = OpenStruct.new(path: 'x') # just a hack to make this work, wish it was cleaner - - element = FHIR::ElementDefinition.new('path' => 'vinv', 'type' => [{ 'code' => 'string' }], 'min' => 1, 'max' => '1') - sd.errors = [] - sd.send(:verify_element, element, 'vinv' => 'string_value') - assert_empty(sd.errors) - - sd.errors = [] - sd.send(:verify_element, element, 'vinv' => 12345) - assert_equal("vinv is not a valid string: '12345'", sd.errors[0]) - assert_equal("vinv did not match one of the valid data types: [\"string\"]", sd.errors[1]) - - element = FHIR::ElementDefinition.new('path' => 'vinv', 'type' => [{ 'code' => 'integer' }], 'min' => 1, 'max' => '1') - sd.errors = [] - sd.send(:verify_element, element, 'vinv' => 12345) - assert_empty(sd.errors) - - sd.errors = [] - sd.send(:verify_element, element, 'vinv' => 'string_value') - assert_equal("vinv is not a valid integer: 'string_value'", sd.errors[0]) - assert_equal("vinv did not match one of the valid data types: [\"integer\"]", sd.errors[1]) - - element = FHIR::ElementDefinition.new('path' => 'vinv', 'type' => [{ 'code' => 'Observation' }], 'min' => 1, 'max' => '1') - sd.errors = [] - sd.send(:verify_element, element, 'vinv' => 'something_that_isnt_an_Observation') - assert_equal("Unable to verify Observation as a FHIR Resource.", sd.errors[0]) - assert_equal("vinv is not a valid Observation: 'something_that_isnt_an_Observation'", sd.errors[1]) - assert_equal("vinv did not match one of the valid data types: [\"Observation\"]", sd.errors[2]) - end - - def test_unable_to_guess_type - sd = FHIR::StructureDefinition.new - sd.warnings = [] - sd.hierarchy = OpenStruct.new(path: 'x') # just a hack - - element = FHIR::ElementDefinition.new('path' => 'no_type', 'min' => 1, 'max' => '1') - sd.send(:verify_element, element, 'no_type' => 'abcd') - assert_equal("Unable to guess data type for no_type", sd.warnings[0]) - - element = FHIR::ElementDefinition.new('path' => 'has_type','type' => [{ 'code' => 'string' }], 'min' => 1, 'max' => '1') - sd.warnings = [] - sd.errors = [] - sd.send(:verify_element, element, 'has_type' => 'abcd') - assert_empty(sd.errors) - assert_empty(sd.warnings) - - # this test is unique in that the message is based purely on the element definition, - # not the object being validated against that definition - end - - def test_valueset_binding - sd = FHIR::StructureDefinition.new - sd.warnings = [] - sd.hierarchy = OpenStruct.new(path: 'x') # just a hack - - # first 2 value sets are special cases - element = FHIR::ElementDefinition.new('path' => 'mime', 'type' => [{ 'code' => 'string' }], 'min' => 1, 'max' => '1', - 'binding' => { 'valueSet' => 'http://hl7.org/fhir/ValueSet/mimetypes' }) - sd.errors = [] - sd.send(:check_binding_element, element, 'xml') - assert_empty(sd.errors) - - sd.send(:check_binding_element, element, 'application/xml') - assert_empty(sd.errors) - - sd.send(:check_binding_element, element, 'jpeg') - assert_equal("mime has invalid mime-type: 'jpeg'", sd.errors[0]) - - element = FHIR::ElementDefinition.new('path' => 'lang', 'type' => [{ 'code' => 'string' }], 'min' => 1, 'max' => '1', - 'binding' => { 'valueSet' => 'http://hl7.org/fhir/ValueSet/languages' }) - sd.errors = [] - sd.send(:check_binding_element, element, 'en') - assert_empty(sd.errors) - - sd.send(:check_binding_element, element, 'English (United States)') - assert_equal("lang has unrecognized language: 'English (United States)'", sd.errors[0]) - - sd.errors = [] - sd.send(:check_binding_element, element, 'qq') - assert_equal("lang has unrecognized language: 'qq'", sd.errors[0]) - - - # use a valueset we don't have defined here - element = FHIR::ElementDefinition.new('path' => 'problem', 'type' => [{ 'code' => 'string' }], 'min' => 1, 'max' => '1', - 'binding' => { 'valueSet' => 'http://standardhealthrecord.org/shr/problem/vs/ProblemCategoryVS' }) - sd.errors = [] - sd.warnings = [] - sd.send(:check_binding_element, element, 'disease') - assert_empty(sd.errors) - assert_equal("problem has unknown ValueSet: 'http://standardhealthrecord.org/shr/problem/vs/ProblemCategoryVS'", sd.warnings[0]) - - - # regular case, FHIR VS with nothing special about it - # binding strength required => error if wrong - element = FHIR::ElementDefinition.new('path' => 'support', 'type' => [{ 'code' => 'string' }], 'min' => 1, 'max' => '1', - 'binding' => { 'valueSet' => 'http://hl7.org/fhir/ValueSet/code-search-support', - 'strength' => 'required' }) - sd.errors = [] - sd.warnings = [] - sd.send(:check_binding_element, element, 'explicit') - assert_empty(sd.errors) - assert_empty(sd.warnings) - - sd.errors = [] - sd.warnings = [] - sd.send(:check_binding_element, element, 'some') - assert_equal("support has invalid code 'some' from http://hl7.org/fhir/ValueSet/code-search-support", sd.errors[0]) - assert_empty(sd.warnings) - - # binding strength example => warning if wrong - element = FHIR::ElementDefinition.new('path' => 'support', 'type' => [{ 'code' => 'string' }], 'min' => 1, 'max' => '1', - 'binding' => { 'valueSet' => 'http://hl7.org/fhir/ValueSet/code-search-support', - 'strength' => 'example' }) - sd.errors = [] - sd.warnings = [] - sd.send(:check_binding_element, element, 'explicit') - assert_empty(sd.errors) - assert_empty(sd.warnings) - - sd.errors = [] - sd.warnings = [] - sd.send(:check_binding_element, element, 'some') - assert_equal("support has invalid code 'some' from http://hl7.org/fhir/ValueSet/code-search-support", sd.warnings[0]) - assert_empty(sd.errors) - end - - def test_get_json_nodes - sd = FHIR::StructureDefinition.new - json = { "colors" => ["red", "green", "blue"], - "people" => [{ "first" => "John", "last" => "Smith"}, { "first" => "Jane", "last" => "Doe"} ], - "nested_level_1" => { "nested_level_2" => { "nested_level_3" => "value"}}} - - nodes = sd.send(:get_json_nodes, json, "colors") - assert_equal(["red", "green", "blue"], nodes) - - nodes = sd.send(:get_json_nodes, json, "people.first") - assert_equal(["John", "Jane"], nodes) - - node = sd.send(:get_json_nodes, json, "nested_level_1.nested_level_2.nested_level_3") - assert_equal(["value"], node) - end end From dfc197b19c68c41ecf2a3c8f938d3fb71461d842 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Thu, 8 Aug 2019 17:18:55 -0400 Subject: [PATCH 34/80] rename method --- lib/fhir_models/validation/structure_validator.rb | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/fhir_models/validation/structure_validator.rb b/lib/fhir_models/validation/structure_validator.rb index f868f5581..2e320e101 100644 --- a/lib/fhir_models/validation/structure_validator.rb +++ b/lib/fhir_models/validation/structure_validator.rb @@ -26,7 +26,7 @@ def initialize(profile, use_default_element_validators: true) def validate(resource) return [FHIR::ValidationResult.new(validation_type: :structure, result: :fail)] unless resource.is_a? FHIR::Model - validate_against_hierarchy(resource) + validate_against_snapshot(resource) FHIR::Validation::StructureValidationResult.new(@snapshot_hierarchy).all_results end @@ -84,7 +84,7 @@ def add_default_element_validators hierarchy end - private def validate_against_hierarchy(resource) + private def validate_against_snapshot(resource) @snapshot_hierarchy ||= build_hierarchy(@profile.snapshot.element) # Slicing is prohibited on first element so we only check the paths # http://www.hl7.org/fhir/elementdefinition.html#interpretation @@ -130,14 +130,6 @@ def add_default_element_validators private def element_path_array(element_definition) element_definition.path.split('.') end - - # This Exception is for indicating types of slices that are not handled. - # - class UnhandledSlice < StandardError - def initialize(msg = 'Unhandled Slice') - super(msg) - end - end end end end From 6c8efddfafb1a607c94f3949413802c586ee0344 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Thu, 8 Aug 2019 17:40:39 -0400 Subject: [PATCH 35/80] remove old validation code in element_definition --- .../fhir_ext/element_definition.rb | 45 ------------------- 1 file changed, 45 deletions(-) diff --git a/lib/fhir_models/fhir_ext/element_definition.rb b/lib/fhir_models/fhir_ext/element_definition.rb index a7bd74f08..101aa9459 100644 --- a/lib/fhir_models/fhir_ext/element_definition.rb +++ b/lib/fhir_models/fhir_ext/element_definition.rb @@ -1,51 +1,6 @@ module FHIR # Extend ElementDefinition for profile validation code class ElementDefinition < FHIR::Model - # children is used to hierarchically arrange elements - # so profile validation is easier to compute - attr_accessor :children - attr_accessor :local_name - attr_accessor :marked_for_keeping - - def add_descendent(element) - @children = [] if @children.nil? - element.local_name = element.path.gsub("#{path}.", '') - if @children.last && element.path.start_with?(@children.last.path) - if element.path == @children.last.path - # slicing - @children << element - else - @children.last.add_descendent(element) - end - else - @children << element - end - end - - def keep_children(whitelist = []) - @marked_for_keeping = true if whitelist.include?(path) - return unless @children - @children.each do |child| - child.keep_children(whitelist) - end - end - - def sweep_children - return unless @children - @children.each(&:sweep_children) - @children = @children.keep_if(&:marked_for_keeping) - @marked_for_keeping = !@children.empty? || @marked_for_keeping - end - - def print_children(spaces = 0) - puts "#{' ' * spaces}+#{local_name || path}" - return nil unless @children - @children.each do |child| - child.print_children(spaces + 2) - end - nil - end - def choice_type? path&.end_with? '[x]' end From 6573838900e8b7659ea82f87dc35ea1db9c5a4f3 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Fri, 9 Aug 2019 10:35:14 -0400 Subject: [PATCH 36/80] update README --- README.md | 42 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 44350096d..09f1acfcd 100644 --- a/README.md +++ b/README.md @@ -57,20 +57,50 @@ $ bundle exec rake fhir:console ### Validation - Using built in validation... + Resources can be validated against the definition of a base resource or profile. All validation methods return a number + `ValidationResult` instances which represent a validation check performed on an element in the resource. Results are + returned for both passing and failing checks. + + Validation against the base resource definition... ```ruby patient.valid? # returns true or false - patient.validate # returns Hash of errors, empty if valid + patient.validate # returns an Array of ValidationResults ``` - Using a profile or structure definition... + Validating with a profile or StructureDefinition... ```ruby sd = FHIR::Definitions.resource_definition('Patient') sd.validates_resource?(patient) # passing in FHIR::Patient - # Validation failed? Get the errors and warnings... - puts sd.errors - puts sd.warnings + sd.validate_resource(patient) # returns an Array of ValidationResults + ``` + + Validation results can be filtered... + ```ruby + # Get cardinality results + results.select {|res| res.validation_type == :cardinality} + + # Get all failing results + results.select {|res| res.result == :fail} ``` + + A customized validator can also be created. + + ```ruby + sd = FHIR::Definitions.resource_definition('Patient') + structure_validator = FHIR::Validation::StructureValidator.new(sd) + + # Remove the default element validators + structure_validator.clear_element_validators + + # Register the cardinality validator + structure_validator.register_element_validators(FHIR::Validation::CardinalityValidator) + + # Add the default element validators + structure_validator.add_default_element_validators + ``` + + A custom element validator that implements the `#validate` method can also be supplied in this way. + # License Copyright 2014-2019 The MITRE Corporation From 7176391a698241bcb7019950b321145f2dbcca80 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Fri, 9 Aug 2019 11:24:46 -0400 Subject: [PATCH 37/80] break out method for getting type and resource definitions --- lib/fhir_models/bootstrap/definitions.rb | 8 ++++++++ lib/fhir_models/bootstrap/model.rb | 2 +- .../element_validator/data_type_validator.rb | 2 +- spec/unit/bootstrap/definitions_spec.rb | 15 +++++++++++++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/fhir_models/bootstrap/definitions.rb b/lib/fhir_models/bootstrap/definitions.rb index e955c0bd9..d07d6a9ed 100644 --- a/lib/fhir_models/bootstrap/definitions.rb +++ b/lib/fhir_models/bootstrap/definitions.rb @@ -13,6 +13,14 @@ class Definitions @@cache = {} + # Return the type or resource definition associated with the type name + # + # @param type_name [String] The name of the type + # @return result [FHIR::StructureDefinition] The StructureDefinition of the type or resource + def self.definition(type_name) + type_definition(type_name) || resource_definition(type_name) + end + # ---------------------------------------------------------------- # Types # ---------------------------------------------------------------- diff --git a/lib/fhir_models/bootstrap/model.rb b/lib/fhir_models/bootstrap/model.rb index 69b32a2b5..91bcbc992 100644 --- a/lib/fhir_models/bootstrap/model.rb +++ b/lib/fhir_models/bootstrap/model.rb @@ -108,7 +108,7 @@ def valid? def validate type_name = self.class.name.split('::').last - type_def = FHIR::Definitions.type_definition(type_name) || FHIR::Definitions.resource_definition(type_name) + type_def = FHIR::Definitions.definition(type_name) FHIR::Validation::StructureValidator.new(type_def).validate(self) end diff --git a/lib/fhir_models/validation/element_validator/data_type_validator.rb b/lib/fhir_models/validation/element_validator/data_type_validator.rb index 35d7765d0..0bdb63909 100644 --- a/lib/fhir_models/validation/element_validator/data_type_validator.rb +++ b/lib/fhir_models/validation/element_validator/data_type_validator.rb @@ -37,7 +37,7 @@ def self.validate_element(element, element_definition, path) /[^.]+$/.match(element_definition.path.gsub('[x]', cap_code)).to_s == /[^.]+$/.match(path).to_s end.code end - type_def = FHIR::Definitions.type_definition(type_code) || FHIR::Definitions.resource_definition(type_code) + type_def = FHIR::Definitions.definition(type_code) # If we are missing the Structure Definition needed to do the validation. if type_def.nil? diff --git a/spec/unit/bootstrap/definitions_spec.rb b/spec/unit/bootstrap/definitions_spec.rb index cf368b544..b22fbefd0 100644 --- a/spec/unit/bootstrap/definitions_spec.rb +++ b/spec/unit/bootstrap/definitions_spec.rb @@ -31,4 +31,19 @@ end end end + + describe '#definition' do + it 'returns resource definitions' do + type_def = FHIR::Definitions.definition('Patient') + expect(type_def).to be_a(FHIR::StructureDefinition) + end + it 'returns primitive-type definitions' do + type_def = FHIR::Definitions.definition('code') + expect(type_def).to be_a(FHIR::StructureDefinition) + end + it 'returns complex-type definitions' do + type_def = FHIR::Definitions.definition('Address') + expect(type_def).to be_a(FHIR::StructureDefinition) + end + end end From 428b2e40f4351fde42c2b07f6ecc3b5de9560e46 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Fri, 9 Aug 2019 11:31:38 -0400 Subject: [PATCH 38/80] rename element_collection to elements in cardinality validator --- .../validation/element_validator/cardinality_validator.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/fhir_models/validation/element_validator/cardinality_validator.rb b/lib/fhir_models/validation/element_validator/cardinality_validator.rb index d9d95d063..157ed2022 100644 --- a/lib/fhir_models/validation/element_validator/cardinality_validator.rb +++ b/lib/fhir_models/validation/element_validator/cardinality_validator.rb @@ -23,14 +23,14 @@ def self.validate(resource, element_definition) end end - def self.validate_element(element_collection, element_definition, path) + def self.validate_element(elements, element_definition, path) min = element_definition.min max = element_definition.max == '*' ? Float::INFINITY : element_definition.max.to_i - cardinality_check = ((element_collection.length < min) || (element_collection.length > max)) + cardinality_check = ((elements.length < min) || (elements.length > max)) FHIR::ValidationResult.new(element_definition: element_definition, validation_type: :cardinality, element_path: path, - element: element_collection, + element: elements, result: cardinality_check ? :fail : :pass) end end From 8fb70e3250cb78c00f003a5564319d2f043009f7 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Fri, 9 Aug 2019 14:08:04 -0400 Subject: [PATCH 39/80] add retrieve_elements_by_definition instance method to FHIR::Models --- lib/fhir_models/bootstrap/model.rb | 12 +++ .../cardinality_validator.rb | 4 +- .../element_validator/data_type_validator.rb | 4 +- .../fixed_value_validator.rb | 4 +- .../element_validator/max_length_validator.rb | 4 +- .../terminology_validator.rb | 4 +- lib/fhir_models/validation/retrieval.rb | 6 ++ .../validation/structure_validator.rb | 3 +- spec/unit/bootstrap/model_spec.rb | 76 +++++++++++++++++++ .../fixed_value_validator_spec.rb | 10 ++- .../max_length_validator_spec.rb | 5 +- spec/unit/validation/retrieval_spec.rb | 4 - 12 files changed, 109 insertions(+), 27 deletions(-) create mode 100644 spec/unit/bootstrap/model_spec.rb diff --git a/lib/fhir_models/bootstrap/model.rb b/lib/fhir_models/bootstrap/model.rb index 91bcbc992..c0c8a543d 100644 --- a/lib/fhir_models/bootstrap/model.rb +++ b/lib/fhir_models/bootstrap/model.rb @@ -17,6 +17,18 @@ def initialize(hash = {}) end end + # Retrieve the elements in the resource which are defined by the provided ElementDefinition + # + # @param element_definition [FHIR::ElementDefinition] The ElementDefinition which defines the desired resources + # @param indexed [Boolean] If the elements should be returned individually or as a collection + # @param normalized [Boolean] If the elements with a choice of type should be normalized + def retrieve_elements_by_definition(element_definition, indexed: false, normalized: false) + FHIR::Validation::Retrieval.retrieve_by_element_definition(self, + element_definition, + indexed: indexed, + normalized: normalized) + end + # This is necessary for uniq to properly identify two FHIR models as being identical def hash to_hash.hash diff --git a/lib/fhir_models/validation/element_validator/cardinality_validator.rb b/lib/fhir_models/validation/element_validator/cardinality_validator.rb index 157ed2022..9b2454cc6 100644 --- a/lib/fhir_models/validation/element_validator/cardinality_validator.rb +++ b/lib/fhir_models/validation/element_validator/cardinality_validator.rb @@ -14,9 +14,7 @@ def self.validate(resource, element_definition) # Zulip Chat: https://chat.fhir.org/#narrow/stream/179166-implementers/topic/cardinality.20of.20root.20elements/near/154024550 return unless element_definition.path.include? '.' - elements = Retrieval.retrieve_by_element_definition(resource, - element_definition, - normalized: true) + elements = resource.retrieve_elements_by_definition(element_definition, normalized: true) elements.flat_map do |path, el| el = [el].flatten.compact validate_element(el, element_definition, path) diff --git a/lib/fhir_models/validation/element_validator/data_type_validator.rb b/lib/fhir_models/validation/element_validator/data_type_validator.rb index 0bdb63909..285f6ea30 100644 --- a/lib/fhir_models/validation/element_validator/data_type_validator.rb +++ b/lib/fhir_models/validation/element_validator/data_type_validator.rb @@ -16,9 +16,7 @@ def self.validate(resource, element_definition) return if element_definition.type.empty? - elements = FHIR::Validation::Retrieval.retrieve_by_element_definition(resource, - element_definition, - indexed: true) + elements = resource.retrieve_elements_by_definition(element_definition, indexed: true) elements.flat_map do |path, el| validate_element(el, element_definition, path) diff --git a/lib/fhir_models/validation/element_validator/fixed_value_validator.rb b/lib/fhir_models/validation/element_validator/fixed_value_validator.rb index 5be37b24a..08d378975 100644 --- a/lib/fhir_models/validation/element_validator/fixed_value_validator.rb +++ b/lib/fhir_models/validation/element_validator/fixed_value_validator.rb @@ -11,9 +11,7 @@ def self.validate(resource, element_definition) fixed = element_definition.fixed return if fixed.respond_to?(:empty?) ? fixed.empty? : fixed.nil? - elements = Retrieval.retrieve_by_element_definition(resource, - element_definition, - indexed: true) + elements = resource.retrieve_elements_by_definition(element_definition, indexed: true) elements.flat_map do |path, el| validate_element(el, element_definition, path) diff --git a/lib/fhir_models/validation/element_validator/max_length_validator.rb b/lib/fhir_models/validation/element_validator/max_length_validator.rb index 780ae41ad..56e463381 100644 --- a/lib/fhir_models/validation/element_validator/max_length_validator.rb +++ b/lib/fhir_models/validation/element_validator/max_length_validator.rb @@ -8,9 +8,7 @@ module MaxLengthValidator def self.validate(resource, element_definition) return if element_definition.maxLength.nil? - elements = Retrieval.retrieve_by_element_definition(resource, - element_definition, - indexed: true) + elements = resource.retrieve_elements_by_definition(element_definition, indexed: true) elements.flat_map do |path, el| validate_element(el, element_definition, path) diff --git a/lib/fhir_models/validation/element_validator/terminology_validator.rb b/lib/fhir_models/validation/element_validator/terminology_validator.rb index 957de6948..2931ec848 100644 --- a/lib/fhir_models/validation/element_validator/terminology_validator.rb +++ b/lib/fhir_models/validation/element_validator/terminology_validator.rb @@ -26,9 +26,7 @@ def clear_vs_validators # @param element_definition [FHIR::ElementDefinition] The Element Definition for the elements # @return result [FHIR::ValidationResult] The result of the terminology check def validate(resource, element_definition) - elements = FHIR::Validation::Retrieval.retrieve_by_element_definition(resource, - element_definition, - indexed: true) + elements = resource.retrieve_elements_by_definition(element_definition, indexed: true) elements.flat_map do |path, el| validate_element(el, element_definition, path) end diff --git a/lib/fhir_models/validation/retrieval.rb b/lib/fhir_models/validation/retrieval.rb index b6c034f37..cf7cf7def 100644 --- a/lib/fhir_models/validation/retrieval.rb +++ b/lib/fhir_models/validation/retrieval.rb @@ -43,6 +43,12 @@ def retrieve_element_with_fhirpath(path, resource, indexed = true) not_indexed end + # Retrieve the elements in the resource which are defined by the provided ElementDefinition + # + # @param resource [FHIR::Model] The resource from which the elements will be retrieved + # @param element_definition [FHIR::ElementDefinition] The ElementDefinition which defines the desired resources + # @param indexed [Boolean] If the elements should be returned individually or as a collection + # @param normalized [Boolean] If the elements with a choice of type should be normalized def retrieve_by_element_definition(resource, element_definition, indexed: false, normalized: false) # Check if we were provided a path that includes extensions (like in the ElementDefinition id versus the path) path = element_definition.path diff --git a/lib/fhir_models/validation/structure_validator.rb b/lib/fhir_models/validation/structure_validator.rb index 2e320e101..a2882aec9 100644 --- a/lib/fhir_models/validation/structure_validator.rb +++ b/lib/fhir_models/validation/structure_validator.rb @@ -108,8 +108,7 @@ def add_default_element_validators hierarchy[:results].push(*results) # Check to see if there are any valid elements to determine if we need to check the subelements - elements = FHIR::Validation::Retrieval.retrieve_by_element_definition(resource, - element_definition) + elements = resource.retrieve_elements_by_definition(element_definition) element_exists = !blank?(elements.values.flatten.compact) # If the element doesn't exist we don't need to check its subelements unless we are instructed to by showskipped diff --git a/spec/unit/bootstrap/model_spec.rb b/spec/unit/bootstrap/model_spec.rb new file mode 100644 index 000000000..ac5726848 --- /dev/null +++ b/spec/unit/bootstrap/model_spec.rb @@ -0,0 +1,76 @@ +describe FHIR::Model do + describe '#retrieve_elements_by_definition' do + let(:resource) do + FHIR::Patient.new(id: 2, + extension: [{url: 'bar'}, {url: 'http://foo.org'}], + name: {given: 'Bob'}, + communication: [{language: 'English'}, {language: 'Spanish'}], + deceasedBoolean: false, + deceasedDateTime: 'YYYY-MM-DD') + end + + it 'returns the root element (the resource)' do + element_definition = FHIR::ElementDefinition.new(id: 'Patient', path: 'Patient') + expect(resource.retrieve_elements_by_definition(element_definition)).to eq({element_definition.id => resource}) + end + + it 'returns an attribute of element' do + element_definition = FHIR::ElementDefinition.new(id: 'Patient.id', path: 'Patient.id') + expect(resource.retrieve_elements_by_definition(element_definition)).to eq({element_definition.id => resource.id}) + end + + context 'with choice of types' do + let(:element_definition) do + FHIR::ElementDefinition.new(id: 'Patient.deceased[x]', + path: 'Patient.deceased[x]', + type: [{code: 'boolean'}, {code: 'dateTime'}]) + end + it 'returns all the elements present if there is a choice of type' do + titlecase_choice = [ + element_definition.id.gsub('[x]', + element_definition.type.first.code.capitalize), + element_definition.id.gsub('[x]', + "#{element_definition.type.last.code[0].capitalize}"\ + "#{element_definition.type.last.code[1..-1]}")] + expected_communications = { + titlecase_choice.first => resource.deceasedBoolean, + titlecase_choice.last => resource.deceasedDateTime + } + expect(resource.retrieve_elements_by_definition(element_definition)).to eq(expected_communications) + end + + it 'returns an array of all the elements present if there is a choice of type normalized to the element' do + expected_communications = { element_definition.id => [ resource.deceasedBoolean, resource.deceasedDateTime ]} + expect(resource.retrieve_elements_by_definition(element_definition, normalized: true)).to eq(expected_communications) + end + end + + context 'with sliced extensions' do + it 'returns all extensions' do + element_definition = FHIR::ElementDefinition.new(id: 'Patient.extension', path: 'Patient.extension') + expect(resource.retrieve_elements_by_definition(element_definition)).to eq({element_definition.id => resource.extension}) + end + it 'returns the extensions indexed' do + expected_result = {'Patient.extension[0]' => resource.extension[0], + 'Patient.extension[1]' => resource.extension[1]} + element_definition = FHIR::ElementDefinition.new(id: 'Patient.extension', path: 'Patient.extension') + expect(resource.retrieve_elements_by_definition(element_definition, indexed: true)).to eq(expected_result) + end + it 'returns the sliced extension' do + element_definition = FHIR::ElementDefinition.new(id: 'Patient.extension:foo', + path: 'Patient.extension', + sliceName: 'foo', + type: [{code: 'Extension', profile: ['http://foo.org']}]) + expect(resource.retrieve_elements_by_definition(element_definition)).to eq({element_definition.path => [resource.extension[1]]}) + end + + it 'returns the sliced extension indexed' do + element_definition = FHIR::ElementDefinition.new(id: 'Patient.extension:foo', + path: 'Patient.extension', + sliceName: 'foo', + type: [{code: 'Extension', profile: ['http://foo.org']}]) + expect(resource.retrieve_elements_by_definition(element_definition, indexed: true)).to eq({"#{element_definition.path}[1]" => resource.extension[1]}) + end + end + end +end \ No newline at end of file diff --git a/spec/unit/validation/element_validator/fixed_value_validator_spec.rb b/spec/unit/validation/element_validator/fixed_value_validator_spec.rb index 9c11d8144..e1af249af 100644 --- a/spec/unit/validation/element_validator/fixed_value_validator_spec.rb +++ b/spec/unit/validation/element_validator/fixed_value_validator_spec.rb @@ -2,10 +2,12 @@ let(:validator) { FHIR::Validation::FixedValueValidator } - let(:element) { '867-5309' } + let(:fixed_value) {'867-5309'} + + let(:element) { FHIR::Patient.new(gender: fixed_value) } #let(:element_definition) { instance_double(FHIR::ElementDefinition) } - let(:element_definition) do FHIR::ElementDefinition.new(id: 'Element', - path: 'Element', + let(:element_definition) do FHIR::ElementDefinition.new(id: 'Patient.gender', + path: 'Patient.gender', type:[{code: 'String'}]) end @@ -14,7 +16,7 @@ # allow(element_definition).to receive(:fixed) # .and_return(element) - element_definition.fixedString = element + element_definition.fixedString = fixed_value results = validator.validate(element, element_definition) expect(results.first.validation_type).to be(:fixed_value) diff --git a/spec/unit/validation/element_validator/max_length_validator_spec.rb b/spec/unit/validation/element_validator/max_length_validator_spec.rb index a031818d4..1a334c3d6 100644 --- a/spec/unit/validation/element_validator/max_length_validator_spec.rb +++ b/spec/unit/validation/element_validator/max_length_validator_spec.rb @@ -1,7 +1,8 @@ describe FHIR::Validation::MaxLengthValidator do let(:validator) {FHIR::Validation::MaxLengthValidator} - let(:resource) { 'foo' } - let(:element_definition) {FHIR::ElementDefinition.new(id: 'Element', path: 'Element')} + let(:restricted_string) {'foo'} + let(:resource) { FHIR::Patient.new(gender: restricted_string) } + let(:element_definition) {FHIR::ElementDefinition.new(id: 'Patient.gender', path: 'Patient.gender')} context 'when the string is shorter than the maxLength' do before do diff --git a/spec/unit/validation/retrieval_spec.rb b/spec/unit/validation/retrieval_spec.rb index a038bf0d0..add4cccdc 100644 --- a/spec/unit/validation/retrieval_spec.rb +++ b/spec/unit/validation/retrieval_spec.rb @@ -89,8 +89,4 @@ end end end - - specify '#retrieve_element_with_fhirpath' do - - end end \ No newline at end of file From 00b0f562f1e90864b41b36027e9ccc6fd7e792e2 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Fri, 9 Aug 2019 14:48:41 -0400 Subject: [PATCH 40/80] add valid_cardinality? method to cardinality validation. add some more docs --- lib/fhir_models/bootstrap/model.rb | 24 +++++++++++++++++-- .../cardinality_validator.rb | 22 +++++++++++++---- .../element_validator/max_length_validator.rb | 7 ++++++ 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/lib/fhir_models/bootstrap/model.rb b/lib/fhir_models/bootstrap/model.rb index c0c8a543d..e0c3b8b22 100644 --- a/lib/fhir_models/bootstrap/model.rb +++ b/lib/fhir_models/bootstrap/model.rb @@ -113,17 +113,37 @@ def compare_attribute(left, right, exclude = []) end end + # Checks if the resource conforms to the base resource StructureDefinition + # @return [Boolean] if the resource is valid def valid? validate.select { |res| res.result == :fail }.empty? end deprecate :is_valid?, :valid? + # Validates the resource against the base resource StructureDefinition + # + # @return [Array] The validation results def validate - type_name = self.class.name.split('::').last - type_def = FHIR::Definitions.definition(type_name) + type_def = type_definition FHIR::Validation::StructureValidator.new(type_def).validate(self) end + # Get the demodulized name of the class + # + # e.g. FHIR::Patient would return: 'Patient' + # @return [String] The name of the FHIR Resource + def type_name + self.class.name.split('::').last + end + + # Get the StructureDefinition of this type + # + # e.g. FHIR::Patient would return the FHIR::StructureDefinition of the Patient resource + # @return [FHIR::StructureDefinition] The StructureDefinition that defines the base resource + def type_definition + FHIR::Definitions.definition(type_name) + end + def primitive?(datatype, value) FHIR.logger.warn("prefer using FHIR.primitive? Called from #{caller.first}") FHIR.primitive?(datatype: datatype, value: value) diff --git a/lib/fhir_models/validation/element_validator/cardinality_validator.rb b/lib/fhir_models/validation/element_validator/cardinality_validator.rb index 9b2454cc6..3e643eb27 100644 --- a/lib/fhir_models/validation/element_validator/cardinality_validator.rb +++ b/lib/fhir_models/validation/element_validator/cardinality_validator.rb @@ -21,16 +21,30 @@ def self.validate(resource, element_definition) end end + # Validates a single element against the ElementDefinition + # + # @param elements [Array] the collection of elements + # @param element_definition [FHIR::ElementDefinition] the ElementDefinition which defines the elements + # @param path [String] where the elements are located in the parent resource def self.validate_element(elements, element_definition, path) - min = element_definition.min - max = element_definition.max == '*' ? Float::INFINITY : element_definition.max.to_i - cardinality_check = ((elements.length < min) || (elements.length > max)) FHIR::ValidationResult.new(element_definition: element_definition, validation_type: :cardinality, element_path: path, element: elements, - result: cardinality_check ? :fail : :pass) + result: valid_cardinality?(elements, element_definition.min, element_definition.max) ? :pass : :fail) + end + + # Determines if the elements meet the cardinality requirements in the ElementDefinition + # + # @param elements [Array] the collection of elements + # @param min [#to_i] the minimum cardinality + # @param max [#to_i] the maximum cardinality + def self.valid_cardinality?(elements, min, max) + min = min.to_i + max = max == '*' ? Float::INFINITY : max.to_i + !((elements.length < min) || (elements.length > max)) end + private_class_method :valid_cardinality? end end end diff --git a/lib/fhir_models/validation/element_validator/max_length_validator.rb b/lib/fhir_models/validation/element_validator/max_length_validator.rb index 56e463381..d59b9af3c 100644 --- a/lib/fhir_models/validation/element_validator/max_length_validator.rb +++ b/lib/fhir_models/validation/element_validator/max_length_validator.rb @@ -2,6 +2,8 @@ module FHIR module Validation # Validate that strings do not exceed the specified max length module MaxLengthValidator + # Verify that the element meets the max length requirement + # # @param element [Object] The Element of the Resource under test # @param element_definition [FHIR::ElementDefinition] The Element Definition from which the max length is derived # @return result [FHIR::ValidationResult] The result of the max length check @@ -15,6 +17,11 @@ def self.validate(resource, element_definition) end end + # Validates a single element against the ElementDefinition + # + # @param elements [Array] the element + # @param element_definition [FHIR::ElementDefinition] the ElementDefinition which defines the element + # @param path [String] where the element is located in the parent resource def self.validate_element(element, element_definition, path) FHIR::ValidationResult.new(element_definition: element_definition, validation_type: :max_length, From 5fc7c392fafcde0602d2e7d909dd4f03970c6cd6 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Fri, 9 Aug 2019 14:51:23 -0400 Subject: [PATCH 41/80] use type_name --- lib/fhir_models/bootstrap/model.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fhir_models/bootstrap/model.rb b/lib/fhir_models/bootstrap/model.rb index e0c3b8b22..70c5d0b0f 100644 --- a/lib/fhir_models/bootstrap/model.rb +++ b/lib/fhir_models/bootstrap/model.rb @@ -71,7 +71,7 @@ def method_missing(method, *_args, &_block) end def to_reference - FHIR::Reference.new(reference: "#{self.class.name.split('::').last}/#{id}") + FHIR::Reference.new(reference: "#{type_name}/#{id}") end def equals?(other, exclude = []) From 0b3fbe079946e30a6b87283d40ad05455084b73d Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Fri, 9 Aug 2019 15:54:50 -0400 Subject: [PATCH 42/80] add type_code method to FHIR::ElementDefinition --- .../fhir_ext/element_definition.rb | 27 +++++++++ .../element_validator/data_type_validator.rb | 18 +----- .../terminology_validator.rb | 11 +--- spec/unit/fhir_ext/element_definition_spec.rb | 55 +++++++++++++++++++ 4 files changed, 84 insertions(+), 27 deletions(-) create mode 100644 spec/unit/fhir_ext/element_definition_spec.rb diff --git a/lib/fhir_models/fhir_ext/element_definition.rb b/lib/fhir_models/fhir_ext/element_definition.rb index 101aa9459..c227174a8 100644 --- a/lib/fhir_models/fhir_ext/element_definition.rb +++ b/lib/fhir_models/fhir_ext/element_definition.rb @@ -1,8 +1,35 @@ module FHIR # Extend ElementDefinition for profile validation code class ElementDefinition < FHIR::Model + # If the element has a choice of types + # @return [Boolean] def choice_type? path&.end_with? '[x]' end + + # Returns the type code + # @param path [String] The path of the element in the resource. Needed to determine the type of elements with a choice of types. + def type_code(element_path = nil) + if type.one? + type.first.code + else + raise UnknownType, 'Need path in order to determine type' unless element_path + + matching_code = type.find do |datatype| + cap_code = "#{datatype.code[0].capitalize}#{datatype.code[1..-1]}" + /[^.]+$/.match(path.gsub('[x]', cap_code)).to_s == /[^.]+$/.match(element_path).to_s + end&.code + + raise UnknownType, "No matching types from #{type.flat_map(&:code)} for element at #{element_path}" if matching_code.nil? + + matching_code + end + end + end + # Error for Unknown Types + class UnknownType < StandardError + def initialize(msg = 'Unknown TypeCode') + super(msg) + end end end diff --git a/lib/fhir_models/validation/element_validator/data_type_validator.rb b/lib/fhir_models/validation/element_validator/data_type_validator.rb index 285f6ea30..d450c632d 100644 --- a/lib/fhir_models/validation/element_validator/data_type_validator.rb +++ b/lib/fhir_models/validation/element_validator/data_type_validator.rb @@ -25,16 +25,7 @@ def self.validate(resource, element_definition) def self.validate_element(element, element_definition, path) # Get the type - type_code = if element_definition.type.one? - element_definition.type.first.code - else - return UnknownType.new('Need path in order to determine type') unless path - - element_definition.type.find do |datatype| - cap_code = "#{datatype.code[0].capitalize}#{datatype.code[1..-1]}" - /[^.]+$/.match(element_definition.path.gsub('[x]', cap_code)).to_s == /[^.]+$/.match(path).to_s - end.code - end + type_code = element_definition.type_code(path) type_def = FHIR::Definitions.definition(type_code) # If we are missing the Structure Definition needed to do the validation. @@ -65,13 +56,6 @@ def self.validate_element(element, element_definition, path) res end end - - # Error for Unknown Types - class UnknownType < StandardError - def initialize(msg = 'Unknown TypeCode') - super(msg) - end - end end end end diff --git a/lib/fhir_models/validation/element_validator/terminology_validator.rb b/lib/fhir_models/validation/element_validator/terminology_validator.rb index 2931ec848..02072777d 100644 --- a/lib/fhir_models/validation/element_validator/terminology_validator.rb +++ b/lib/fhir_models/validation/element_validator/terminology_validator.rb @@ -35,16 +35,7 @@ def validate(resource, element_definition) def validate_element(element, element_definition, path) results = [] # Get the type - type_code = if element_definition.type.one? - element_definition.type.first.code - else - return UnknownType.new('Need path in order to determine type') unless path - - element_definition.type.find do |datatype| - cap_code = "#{datatype.code[0].capitalize}#{datatype.code[1..-1]}" - /[^.]+$/.match(element_definition.path.gsub('[x]', cap_code)) == /[^.]+$/.match(path) - end.code - end + type_code = element_definition.type_code(path) if %w[CodeableConcept Coding Quantity].include? type_code required_strength = element_definition&.binding&.strength == 'required' diff --git a/spec/unit/fhir_ext/element_definition_spec.rb b/spec/unit/fhir_ext/element_definition_spec.rb new file mode 100644 index 000000000..916143de7 --- /dev/null +++ b/spec/unit/fhir_ext/element_definition_spec.rb @@ -0,0 +1,55 @@ +describe FHIR::ElementDefinition do + + shared_context 'choice of type' do + let(:element_definition) do + FHIR::ElementDefinition.new(path: 'Patient.deceased[x]', type: [{code: 'boolean'}, {code: 'dateTime'}]) + end + end + + shared_context 'single type' do + let(:element_definition) do + FHIR::ElementDefinition.new(path: 'Patient.deceased', type: [{code: 'boolean'}]) + end + end + describe '#choice_type' do + context 'with a choice of types' do + include_context 'choice of type' + it 'indicates there is a choice of type' do + expect(element_definition.choice_type?).to be true + end + end + + context 'with a single type' do + include_context 'single type' + it 'indicates there is not a choice of types' do + expect(element_definition.choice_type?).to be false + end + end + end + + describe '#type_code' do + context 'with a single type' do + include_context 'single type' + it 'provides the expected type of the element' do + expect(element_definition.type_code).to eq('boolean') + expect(element_definition.type_code('fake.path')).to eq('boolean') + end + end + context 'with a choice of types' do + include_context 'choice of type' + it 'provides the correct type from the choices' do + expect(element_definition.type_code('Patient.deceasedBoolean')).to eq('boolean') + expect(element_definition.type_code('Patient.deceasedDateTime')).to eq('dateTime') + end + + it 'returns an UnknownType exception if path is not provided' do + expect { element_definition.type_code }.to raise_error(FHIR::UnknownType) + end + + it 'returns an UnknownType exception if none of the types match' do + expect { element_definition.type_code('Patient.deceasedBoatLength') }.to raise_error(FHIR::UnknownType, + 'No matching types from ["boolean", "dateTime"] for element at Patient.deceasedBoatLength') + end + end + end +end \ No newline at end of file From fbefe1fcebb0a20397cf69a6d3357bfa47d74c41 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Fri, 9 Aug 2019 16:11:20 -0400 Subject: [PATCH 43/80] move result text into variable --- .../element_validator/data_type_validator.rb | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/fhir_models/validation/element_validator/data_type_validator.rb b/lib/fhir_models/validation/element_validator/data_type_validator.rb index d450c632d..27db3c316 100644 --- a/lib/fhir_models/validation/element_validator/data_type_validator.rb +++ b/lib/fhir_models/validation/element_validator/data_type_validator.rb @@ -25,19 +25,24 @@ def self.validate(resource, element_definition) def self.validate_element(element, element_definition, path) # Get the type - type_code = element_definition.type_code(path) + begin + type_code = element_definition.type_code(path) + rescue FHIR::UnknownType + type_code = nil + end type_def = FHIR::Definitions.definition(type_code) # If we are missing the Structure Definition needed to do the validation. if type_def.nil? + result_text = if type_code.nil? + 'Type code not provided or extends the primitive code' + else + "Unknown type: #{type_code}" + end return FHIR::ValidationResult.new(element_definition: element_definition, validation_type: :datatype, result: :warn, - text: (if type_code.nil? - 'Type code not provided or extends the primitive code' - else - "Unknown type: #{type_code}" - end), + text: result_text, element_path: path || element_definition.path) elsif type_def.kind == 'primitive-type' return FHIR::ValidationResult.new(element_definition: element_definition, From b669055dfa799ba13434a7aee33e3735f1684d63 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Fri, 9 Aug 2019 16:35:37 -0400 Subject: [PATCH 44/80] remove warnings for checking structure of primitive-types --- .../cardinality_validator.rb | 2 ++ .../element_validator/data_type_validator.rb | 30 ++++++++++--------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/lib/fhir_models/validation/element_validator/cardinality_validator.rb b/lib/fhir_models/validation/element_validator/cardinality_validator.rb index 3e643eb27..5536cbf20 100644 --- a/lib/fhir_models/validation/element_validator/cardinality_validator.rb +++ b/lib/fhir_models/validation/element_validator/cardinality_validator.rb @@ -26,6 +26,7 @@ def self.validate(resource, element_definition) # @param elements [Array] the collection of elements # @param element_definition [FHIR::ElementDefinition] the ElementDefinition which defines the elements # @param path [String] where the elements are located in the parent resource + # @return result [FHIR::ValidationResult] the result of the cardinality check on the elements def self.validate_element(elements, element_definition, path) FHIR::ValidationResult.new(element_definition: element_definition, validation_type: :cardinality, @@ -39,6 +40,7 @@ def self.validate_element(elements, element_definition, path) # @param elements [Array] the collection of elements # @param min [#to_i] the minimum cardinality # @param max [#to_i] the maximum cardinality + # @return result [Boolean] if the elements meet the cardinality requirements def self.valid_cardinality?(elements, min, max) min = min.to_i max = max == '*' ? Float::INFINITY : max.to_i diff --git a/lib/fhir_models/validation/element_validator/data_type_validator.rb b/lib/fhir_models/validation/element_validator/data_type_validator.rb index 27db3c316..eab61c98e 100644 --- a/lib/fhir_models/validation/element_validator/data_type_validator.rb +++ b/lib/fhir_models/validation/element_validator/data_type_validator.rb @@ -23,33 +23,35 @@ def self.validate(resource, element_definition) end end + # Validates a single element against the ElementDefinition + # + # @param elements [Array] the element to be validated + # @param element_definition [FHIR::ElementDefinition] the ElementDefinition which defines the element + # @param path [String] where the element is located in the parent resource + # @return result [Array] the result of the data type validation def self.validate_element(element, element_definition, path) # Get the type begin type_code = element_definition.type_code(path) - rescue FHIR::UnknownType - type_code = nil + type_def = FHIR::Definitions.definition(type_code) + rescue FHIR::UnknownType => ex + return FHIR::ValidationResult.new(element_definition: element_definition, + validation_type: :datatype, + result: :warn, + text: ex.message, + element_path: path || element_definition.path) end - type_def = FHIR::Definitions.definition(type_code) # If we are missing the Structure Definition needed to do the validation. if type_def.nil? - result_text = if type_code.nil? - 'Type code not provided or extends the primitive code' - else - "Unknown type: #{type_code}" - end return FHIR::ValidationResult.new(element_definition: element_definition, validation_type: :datatype, result: :warn, - text: result_text, + text: "Unknown type: #{type_code}", element_path: path || element_definition.path) elsif type_def.kind == 'primitive-type' - return FHIR::ValidationResult.new(element_definition: element_definition, - validation_type: :datatype, - result: :warn, - text: 'Cannot validate primitive-type as a Structure', - element_path: path || element_definition.path) + # We do not support primitive extensions or check their structure + return end type_validator = FHIR::Validation::StructureValidator.new(type_def) type_validator.register_element_validators(FHIR::Validation::CardinalityValidator) From 67035d22391b3d663a65cdf369df0be808125a6f Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Fri, 9 Aug 2019 16:48:11 -0400 Subject: [PATCH 45/80] data_type_validator map to each. more docs. --- .../element_validator/cardinality_validator.rb | 2 +- .../element_validator/data_type_validator.rb | 7 +++---- .../element_validator/fixed_value_validator.rb | 8 +++++++- .../element_validator/max_length_validator.rb | 5 +++-- .../element_validator/terminology_validator.rb | 11 +++++++++-- 5 files changed, 23 insertions(+), 10 deletions(-) diff --git a/lib/fhir_models/validation/element_validator/cardinality_validator.rb b/lib/fhir_models/validation/element_validator/cardinality_validator.rb index 5536cbf20..ce2b37ed8 100644 --- a/lib/fhir_models/validation/element_validator/cardinality_validator.rb +++ b/lib/fhir_models/validation/element_validator/cardinality_validator.rb @@ -4,7 +4,7 @@ module Validation module CardinalityValidator # Verify that the element meets the cardinality requirements # - # @param element [Object] The Element of the Resource under test + # @param resource [FHIR::Model] The resource under test # @param element_definition [FHIR::ElementDefinition] The Element Definition from which the cardinality is taken # @return result [FHIR::ValidationResult] The result of the cardinality check def self.validate(resource, element_definition) diff --git a/lib/fhir_models/validation/element_validator/data_type_validator.rb b/lib/fhir_models/validation/element_validator/data_type_validator.rb index eab61c98e..8c456b52f 100644 --- a/lib/fhir_models/validation/element_validator/data_type_validator.rb +++ b/lib/fhir_models/validation/element_validator/data_type_validator.rb @@ -8,7 +8,7 @@ module DataTypeValidator # structures it is composed of. # # https://www.hl7.org/fhir/datatypes.html - # @param element [Object] The Element of the Resource under test + # @param resource [FHIR::Model] The resource under test # @param element_definition [FHIR::ElementDefinition] The Element Definition from which the cardinality is taken # @return result [FHIR::ValidationResult] The result of the cardinality check def self.validate(resource, element_definition) @@ -25,7 +25,7 @@ def self.validate(resource, element_definition) # Validates a single element against the ElementDefinition # - # @param elements [Array] the element to be validated + # @param element [FHIR::Model] the element to be validated # @param element_definition [FHIR::ElementDefinition] the ElementDefinition which defines the element # @param path [String] where the element is located in the parent resource # @return result [Array] the result of the data type validation @@ -58,9 +58,8 @@ def self.validate_element(element, element_definition, path) results = type_validator.validate(element) # Update the path to reflect the object it is nested within - results.map do |res| + results.each do |res| res.element_path = res.element_path.gsub(/^([^.]+)/, path) - res end end end diff --git a/lib/fhir_models/validation/element_validator/fixed_value_validator.rb b/lib/fhir_models/validation/element_validator/fixed_value_validator.rb index 08d378975..4d61e5c69 100644 --- a/lib/fhir_models/validation/element_validator/fixed_value_validator.rb +++ b/lib/fhir_models/validation/element_validator/fixed_value_validator.rb @@ -4,7 +4,7 @@ module Validation module FixedValueValidator # Validate the element matches the fixed value if a fixed value is provided # - # @param element [Object] The Element of the Resource under test + # @param resource [FHIR::Model] The resource under test # @param element_definition [FHIR::ElementDefinition] The Element Definition from which the cardinality is taken # @return result [FHIR::ValidationResult] The result of the cardinality check def self.validate(resource, element_definition) @@ -18,6 +18,12 @@ def self.validate(resource, element_definition) end end + # Validates a single element against the ElementDefinition + # + # @param element [] the element to be validated + # @param element_definition [FHIR::ElementDefinition] the ElementDefinition which defines the element + # @param path [String] where the element is located in the parent resource + # @return result [FHIR::ValidationResult] the result of the validation def self.validate_element(element, element_definition, path) FHIR::ValidationResult.new(element_definition: element_definition, validation_type: :fixed_value, diff --git a/lib/fhir_models/validation/element_validator/max_length_validator.rb b/lib/fhir_models/validation/element_validator/max_length_validator.rb index d59b9af3c..709b22039 100644 --- a/lib/fhir_models/validation/element_validator/max_length_validator.rb +++ b/lib/fhir_models/validation/element_validator/max_length_validator.rb @@ -4,7 +4,7 @@ module Validation module MaxLengthValidator # Verify that the element meets the max length requirement # - # @param element [Object] The Element of the Resource under test + # @param resource [FHIR::Model] The resource under test # @param element_definition [FHIR::ElementDefinition] The Element Definition from which the max length is derived # @return result [FHIR::ValidationResult] The result of the max length check def self.validate(resource, element_definition) @@ -19,9 +19,10 @@ def self.validate(resource, element_definition) # Validates a single element against the ElementDefinition # - # @param elements [Array] the element + # @param element [#length] the string # @param element_definition [FHIR::ElementDefinition] the ElementDefinition which defines the element # @param path [String] where the element is located in the parent resource + # @return result [FHIR::ValidationResult] the result of the validation def self.validate_element(element, element_definition, path) FHIR::ValidationResult.new(element_definition: element_definition, validation_type: :max_length, diff --git a/lib/fhir_models/validation/element_validator/terminology_validator.rb b/lib/fhir_models/validation/element_validator/terminology_validator.rb index 02072777d..75c4dc415 100644 --- a/lib/fhir_models/validation/element_validator/terminology_validator.rb +++ b/lib/fhir_models/validation/element_validator/terminology_validator.rb @@ -22,9 +22,9 @@ def clear_vs_validators # Verify that the element meets terminology requirements # - # @param element [Object] The Element of the Resource under test + # @param resource [FHIR::Model] The resource under test # @param element_definition [FHIR::ElementDefinition] The Element Definition for the elements - # @return result [FHIR::ValidationResult] The result of the terminology check + # @return result [Array] The result of the terminology check def validate(resource, element_definition) elements = resource.retrieve_elements_by_definition(element_definition, indexed: true) elements.flat_map do |path, el| @@ -32,6 +32,13 @@ def validate(resource, element_definition) end end + + # Validates a single element against the ElementDefinition + # + # @param element [] the element to be validated + # @param element_definition [FHIR::ElementDefinition] the ElementDefinition which defines the element + # @param path [String] where the element is located in the parent resource + # @return result [Array] the result of the data type validation def validate_element(element, element_definition, path) results = [] # Get the type From ad58e9c3025a966d9fa4a211baf258a8db3a9f04 Mon Sep 17 00:00:00 2001 From: Reece Adamson <41651655+radamson@users.noreply.github.com> Date: Fri, 9 Aug 2019 16:49:37 -0400 Subject: [PATCH 46/80] cardinality validator method docs update Co-Authored-By: Stephen MacVicar --- .../validation/element_validator/cardinality_validator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fhir_models/validation/element_validator/cardinality_validator.rb b/lib/fhir_models/validation/element_validator/cardinality_validator.rb index ce2b37ed8..4a964d02d 100644 --- a/lib/fhir_models/validation/element_validator/cardinality_validator.rb +++ b/lib/fhir_models/validation/element_validator/cardinality_validator.rb @@ -6,7 +6,7 @@ module CardinalityValidator # # @param resource [FHIR::Model] The resource under test # @param element_definition [FHIR::ElementDefinition] The Element Definition from which the cardinality is taken - # @return result [FHIR::ValidationResult] The result of the cardinality check + # @return result [Array] The result of the cardinality check def self.validate(resource, element_definition) # Cardinality has no meaning on the first element. # It is listed as optional(irrelevant) From eab92ad678d89097ed1d6755945896674d5bb43a Mon Sep 17 00:00:00 2001 From: Reece Adamson <41651655+radamson@users.noreply.github.com> Date: Fri, 9 Aug 2019 16:58:53 -0400 Subject: [PATCH 47/80] @vs_validators to vs_validators Co-Authored-By: Stephen MacVicar --- .../validation/element_validator/terminology_validator.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fhir_models/validation/element_validator/terminology_validator.rb b/lib/fhir_models/validation/element_validator/terminology_validator.rb index 75c4dc415..1aaf95b86 100644 --- a/lib/fhir_models/validation/element_validator/terminology_validator.rb +++ b/lib/fhir_models/validation/element_validator/terminology_validator.rb @@ -67,7 +67,7 @@ def validate_element(element, element_definition, path) end check = lambda do |uri, fail_level| - check_fn = @vs_validators[uri] + check_fn = vs_validators[uri] return result.call(:warn, "Missing Validator for #{uri}") unless check_fn return result.call(fail_level, "#{path} has no codings from #{uri}.") unless check_fn.call(coding) From 64f69c474b923b18a7fbed9949df5cad2fbda094 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Mon, 12 Aug 2019 12:20:00 -0400 Subject: [PATCH 48/80] break up terminology validator --- .../element_validator/code_validator.rb | 109 ++++++++++++++++++ .../terminology_validator.rb | 60 +--------- lib/fhir_models/validation/retrieval.rb | 26 ++--- 3 files changed, 126 insertions(+), 69 deletions(-) create mode 100644 lib/fhir_models/validation/element_validator/code_validator.rb diff --git a/lib/fhir_models/validation/element_validator/code_validator.rb b/lib/fhir_models/validation/element_validator/code_validator.rb new file mode 100644 index 000000000..e337bcfa0 --- /dev/null +++ b/lib/fhir_models/validation/element_validator/code_validator.rb @@ -0,0 +1,109 @@ +module FHIR + module Validation + # Validates a coded element + class CodeValidator + attr_accessor :results + attr_reader :element_definition, :element, :path, :validator + + def initialize(element, element_definition, path, validator) + @element = element + @element_definition = element_definition + @path = path + @validator = validator + end + + # Validate a coded element + # + # @return result [Array] the result of the data type validation + def validate + results = [] + if %w[CodeableConcept Coding Quantity].include? type_code + if type_code == 'CodeableConcept' + element.coding.each do |coding| + results.concat(check_code(coding, valueset_uri)) + 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 + results.concat(check_code(element, valueset_uri)) unless element_definition.path.include? 'CodeableConcept.coding' + end + end + results + end + + # Returns the type of the element based off the element definition and path + # + # This is useful for determining the type when a choice of types are present. + # + # @return type_code [String] the type of the element + private def type_code + element_definition.type_code(path) + end + + # If the binding is set to 'required' + # + # @return required_binding? [Boolean] if the binding is set to 'required' + private def required_binding? + element_definition&.binding&.strength == 'required' + end + + # The URI of the bound ValueSet + # + # @return valueset_uri [String] + private def valueset_uri + element_definition&.binding&.valueSet + end + + # if failures return a warn of fail result + # + # @return fail_level [:fail, :warn] + private def fail_level + required_binding? ? :fail : :warn + end + + # Check the code for an individually element + # + # @param coding [#code & #system] the coded element containing a code and system + # @param valueset_uri [String] the valueset uri + private def check_code(coding, valueset_uri) + check_results = [] + # Can't validate if both code and system are not given + if coding.code.nil? || coding.system.nil? + check_results.push(new_result(:warn, "#{path}: missing code")) if coding.code.nil? + check_results.push(new_result(:warn, "#{path}: missing system")) if coding.system.nil? + return check_results + end + + # ValueSet Validation + check_results.push(validate_code(coding, valueset_uri, fail_level)) + # Code System Validation + check_results.push(validate_code(coding, coding.system, :fail)) + check_results + end + + # Validate against a registered ValueSet Validator + # + # @param coding [#code & #system] the coded element containing a code and system + # @param uri [String] the uri for the ValueSet Validator + private def validate_code(coding, uri, fail_level) + check_fn = validator.vs_validators[uri] + return new_result(:warn, "Missing Validator for #{uri}") unless check_fn + + return new_result(fail_level, "#{path} has no codings from #{uri}.") unless check_fn.call(coding) + + new_result(:pass, "#{path} has codings from #{uri}.") + end + + # Validation result for the code validation + # @return result [FHIR::ValidationResult] + private def new_result(result_status, message) + FHIR::ValidationResult.new(element_definition: element_definition, + validation_type: :terminology, + element_path: path, + element: element, + text: message, + result: result_status) + end + end + end +end diff --git a/lib/fhir_models/validation/element_validator/terminology_validator.rb b/lib/fhir_models/validation/element_validator/terminology_validator.rb index 1aaf95b86..e2c285790 100644 --- a/lib/fhir_models/validation/element_validator/terminology_validator.rb +++ b/lib/fhir_models/validation/element_validator/terminology_validator.rb @@ -27,71 +27,19 @@ def clear_vs_validators # @return result [Array] The result of the terminology check def validate(resource, element_definition) elements = resource.retrieve_elements_by_definition(element_definition, indexed: true) - elements.flat_map do |path, el| - validate_element(el, element_definition, path) + elements.flat_map do |path, element| + validate_element(element, element_definition, path) end end - # Validates a single element against the ElementDefinition # # @param element [] the element to be validated # @param element_definition [FHIR::ElementDefinition] the ElementDefinition which defines the element # @param path [String] where the element is located in the parent resource - # @return result [Array] the result of the data type validation + # @return result [Array] the result of the terminology type validation def validate_element(element, element_definition, path) - results = [] - # Get the type - type_code = element_definition.type_code(path) - - if %w[CodeableConcept Coding Quantity].include? type_code - required_strength = element_definition&.binding&.strength == 'required' - - result = lambda do |result_status, message| - FHIR::ValidationResult.new(element_definition: element_definition, - validation_type: :terminology, - element_path: path, - element: element, - text: message, - result: result_status) - end - - valueset_uri = element_definition&.binding&.valueSet - - check_code = lambda do |coding, fail_strength| - # Can't validate if both code and system are not given - if coding.code.nil? || coding.system.nil? - results.push(result.call(:warn, "#{path}: missing code")) if coding.code.nil? - results.push(result.call(:warn, "#{path}: missing system")) if coding.system.nil? - return results - end - - check = lambda do |uri, fail_level| - check_fn = vs_validators[uri] - return result.call(:warn, "Missing Validator for #{uri}") unless check_fn - - return result.call(fail_level, "#{path} has no codings from #{uri}.") unless check_fn.call(coding) - - return result.call(:pass, "#{path} has codings from #{uri}.") - end - - # ValueSet Validation - results.push(check.call(valueset_uri, fail_strength)) - # Code System Validation - results.push(check.call(coding.system, :fail)) - end - - if type_code == 'CodeableConcept' - element.coding.each do |coding| - check_code.call(coding, required_strength ? :fail : :warn) - 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(element, required_strength ? :fail : :warn) unless element_definition.path.include? 'CodeableConcept.coding' - end - end - results + CodeValidator.new(element, element_definition, path, self).validate end end end diff --git a/lib/fhir_models/validation/retrieval.rb b/lib/fhir_models/validation/retrieval.rb index cf7cf7def..93b6f9154 100644 --- a/lib/fhir_models/validation/retrieval.rb +++ b/lib/fhir_models/validation/retrieval.rb @@ -8,27 +8,27 @@ module Retrieval # @param resource [FHIR::Model] the resource from which the elements will be retrieved # @return [Hash] The desired elements def retrieve_element_with_fhirpath(path, resource, indexed = true) - spath = path.split('.') - base = spath.shift - fhirpath_elements = { base => resource } - last = spath.pop unless indexed + path_parts = path.split('.') + base_path = path_parts.shift + fhir_path_elements = { base_path => resource } + last = path_parts.pop unless indexed - desired_elements = spath.inject(fhirpath_elements) do |memo, meth| - digging = {} - memo.each do |k, v| - fix_name = %w[class method resourceType].include?(meth) ? "local_#{meth}" : meth - elms = v.send(fix_name) if v.is_a? FHIR::Model # FHIR Primitives are not modeled and will throw NoMethod Error + desired_elements = path_parts.reduce(fhir_path_elements) do |parent, sub_path| + children = {} + parent.each do |parent_path, parent_value| + fix_name = %w[class method resourceType].include?(sub_path) ? "local_#{sub_path}" : sub_path + elms = parent_value.send(fix_name) if parent_value.is_a? FHIR::Model # FHIR Primitives are not modeled and will throw NoMethod Error # More than one element where the FHIRPath needs indexing if elms.respond_to? :each_with_index - elms.each_with_index do |vv, kk| - digging["#{k}.#{meth}[#{kk}]"] = vv unless blank?(vv) + elms.each_with_index do |indexed_element_value, indexed_element_path| + children["#{parent_path}.#{sub_path}[#{indexed_element_path}]"] = indexed_element_value unless blank?(indexed_element_value) end # Just One else - digging["#{k}.#{meth}"] = elms unless blank?(elms) + children["#{parent_path}.#{sub_path}"] = elms unless blank?(elms) end end - digging + children end return desired_elements unless last From 2707ffc173ea48311b3f6a12011988ad33430d5c Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Mon, 12 Aug 2019 12:26:04 -0400 Subject: [PATCH 49/80] fix_name to fixed_name --- lib/fhir_models/validation/retrieval.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/fhir_models/validation/retrieval.rb b/lib/fhir_models/validation/retrieval.rb index 93b6f9154..3b04404b3 100644 --- a/lib/fhir_models/validation/retrieval.rb +++ b/lib/fhir_models/validation/retrieval.rb @@ -16,8 +16,8 @@ def retrieve_element_with_fhirpath(path, resource, indexed = true) desired_elements = path_parts.reduce(fhir_path_elements) do |parent, sub_path| children = {} parent.each do |parent_path, parent_value| - fix_name = %w[class method resourceType].include?(sub_path) ? "local_#{sub_path}" : sub_path - elms = parent_value.send(fix_name) if parent_value.is_a? FHIR::Model # FHIR Primitives are not modeled and will throw NoMethod Error + fixed_name = %w[class method resourceType].include?(sub_path) ? "local_#{sub_path}" : sub_path + elms = parent_value.send(fixed_name) if parent_value.is_a? FHIR::Model # FHIR Primitives are not modeled and will throw NoMethod Error # More than one element where the FHIRPath needs indexing if elms.respond_to? :each_with_index elms.each_with_index do |indexed_element_value, indexed_element_path| @@ -36,8 +36,8 @@ def retrieve_element_with_fhirpath(path, resource, indexed = true) # If we don't want to index the last element (useful for testing cardinality) not_indexed = {} desired_elements.each do |k, v| - fix_name = %w[class method resourceType].include?(last) ? "local_#{last}" : last - elms = v.send(fix_name) if v.is_a? FHIR::Model # FHIR Primitives are not modeled and will throw NoMethod Error + fixed_name = %w[class method resourceType].include?(last) ? "local_#{last}" : last + elms = v.send(fixed_name) if v.is_a? FHIR::Model # FHIR Primitives are not modeled and will throw NoMethod Error not_indexed["#{k}.#{last}"] = elms end not_indexed From 90231aa21de8c38b65f992f311c10268cad96a96 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Mon, 12 Aug 2019 12:33:09 -0400 Subject: [PATCH 50/80] better variable names --- lib/fhir_models/validation/retrieval.rb | 30 ++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/fhir_models/validation/retrieval.rb b/lib/fhir_models/validation/retrieval.rb index 3b04404b3..3a3f98715 100644 --- a/lib/fhir_models/validation/retrieval.rb +++ b/lib/fhir_models/validation/retrieval.rb @@ -17,15 +17,15 @@ def retrieve_element_with_fhirpath(path, resource, indexed = true) children = {} parent.each do |parent_path, parent_value| fixed_name = %w[class method resourceType].include?(sub_path) ? "local_#{sub_path}" : sub_path - elms = parent_value.send(fixed_name) if parent_value.is_a? FHIR::Model # FHIR Primitives are not modeled and will throw NoMethod Error + elements = parent_value.send(fixed_name) if parent_value.is_a? FHIR::Model # FHIR Primitives are not modeled and will throw NoMethod Error # More than one element where the FHIRPath needs indexing - if elms.respond_to? :each_with_index - elms.each_with_index do |indexed_element_value, indexed_element_path| + if elements.respond_to? :each_with_index + elements.each_with_index do |indexed_element_value, indexed_element_path| children["#{parent_path}.#{sub_path}[#{indexed_element_path}]"] = indexed_element_value unless blank?(indexed_element_value) end # Just One else - children["#{parent_path}.#{sub_path}"] = elms unless blank?(elms) + children["#{parent_path}.#{sub_path}"] = elements unless blank?(elements) end end children @@ -35,10 +35,10 @@ def retrieve_element_with_fhirpath(path, resource, indexed = true) # If we don't want to index the last element (useful for testing cardinality) not_indexed = {} - desired_elements.each do |k, v| + desired_elements.each do |current_path, element| fixed_name = %w[class method resourceType].include?(last) ? "local_#{last}" : last - elms = v.send(fixed_name) if v.is_a? FHIR::Model # FHIR Primitives are not modeled and will throw NoMethod Error - not_indexed["#{k}.#{last}"] = elms + elements = element.send(fixed_name) if element.is_a? FHIR::Model # FHIR Primitives are not modeled and will throw NoMethod Error + not_indexed["#{current_path}.#{last}"] = elements end not_indexed end @@ -63,11 +63,11 @@ def retrieve_by_element_definition(resource, element_definition, indexed: false, end if normalized choice_type_elements = {} - elements.each do |k, v| - renorm = k.rpartition('.').first + elements.each do |element_path, element| + renorm = element_path.rpartition('.').first normalized_path = "#{renorm}.#{element_definition.path.split('.').last}" choice_type_elements[normalized_path] ||= [] - choice_type_elements[normalized_path].push(v).compact! + choice_type_elements[normalized_path].push(element).compact! end elements = choice_type_elements end @@ -84,14 +84,14 @@ def retrieve_by_element_definition(resource, element_definition, indexed: false, # Only select the elements which match the slice profile. if indexed # Elements are already indexed - elements.select! do |_k, v| - v.url == element_definition.type.first.profile.first + elements.select! do |_k, element| + element.url == element_definition.type.first.profile.first end else sliced_elements = {} - elements.each do |k, v| - sliced_elements[k] = v.select do |ext| - ext.url == element_definition.type.first.profile.first + elements.each do |element_path, element| + sliced_elements[element_path] = element.select do |extension| + extension.url == element_definition.type.first.profile.first end end elements = sliced_elements From ba3ef469fefa88e6044a15a09721c9289ea25a61 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Mon, 12 Aug 2019 13:39:15 -0400 Subject: [PATCH 51/80] add method for getting paths with type_code --- lib/fhir_models/fhir_ext/element_definition.rb | 15 +++++++++++++-- lib/fhir_models/validation/retrieval.rb | 5 ++--- spec/unit/fhir_ext/element_definition_spec.rb | 15 +++++++++++++++ spec/unit/validation/retrieval_spec.rb | 11 +++++++++++ 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/lib/fhir_models/fhir_ext/element_definition.rb b/lib/fhir_models/fhir_ext/element_definition.rb index c227174a8..93efea5ac 100644 --- a/lib/fhir_models/fhir_ext/element_definition.rb +++ b/lib/fhir_models/fhir_ext/element_definition.rb @@ -16,8 +16,7 @@ def type_code(element_path = nil) raise UnknownType, 'Need path in order to determine type' unless element_path matching_code = type.find do |datatype| - cap_code = "#{datatype.code[0].capitalize}#{datatype.code[1..-1]}" - /[^.]+$/.match(path.gsub('[x]', cap_code)).to_s == /[^.]+$/.match(element_path).to_s + /[^.]+$/.match(path.gsub('[x]', capitalize(datatype))).to_s == /[^.]+$/.match(element_path).to_s end&.code raise UnknownType, "No matching types from #{type.flat_map(&:code)} for element at #{element_path}" if matching_code.nil? @@ -25,6 +24,18 @@ def type_code(element_path = nil) matching_code end end + + # Returns the potential paths of the element for each possible type + # @return type_paths [Array] the possible paths + def type_paths + type.map do |data_type| + path.gsub('[x]', capitalize(data_type)) + end + end + + private def capitalize(string) + "#{string.code[0].capitalize}#{string.code[1..-1]}" + end end # Error for Unknown Types class UnknownType < StandardError diff --git a/lib/fhir_models/validation/retrieval.rb b/lib/fhir_models/validation/retrieval.rb index 3a3f98715..d944dce0b 100644 --- a/lib/fhir_models/validation/retrieval.rb +++ b/lib/fhir_models/validation/retrieval.rb @@ -7,7 +7,7 @@ module Retrieval # @param path [String] the path # @param resource [FHIR::Model] the resource from which the elements will be retrieved # @return [Hash] The desired elements - def retrieve_element_with_fhirpath(path, resource, indexed = true) + def self.retrieve_element_with_fhirpath(path, resource, indexed = true) path_parts = path.split('.') base_path = path_parts.shift fhir_path_elements = { base_path => resource } @@ -49,7 +49,7 @@ def retrieve_element_with_fhirpath(path, resource, indexed = true) # @param element_definition [FHIR::ElementDefinition] The ElementDefinition which defines the desired resources # @param indexed [Boolean] If the elements should be returned individually or as a collection # @param normalized [Boolean] If the elements with a choice of type should be normalized - def retrieve_by_element_definition(resource, element_definition, indexed: false, normalized: false) + def self.retrieve_by_element_definition(resource, element_definition, indexed: false, normalized: false) # Check if we were provided a path that includes extensions (like in the ElementDefinition id versus the path) path = element_definition.path @@ -105,7 +105,6 @@ def self.blank?(obj) end private_class_method :blank? - module_function :retrieve_by_element_definition, :retrieve_element_with_fhirpath end end end diff --git a/spec/unit/fhir_ext/element_definition_spec.rb b/spec/unit/fhir_ext/element_definition_spec.rb index 916143de7..e78b17249 100644 --- a/spec/unit/fhir_ext/element_definition_spec.rb +++ b/spec/unit/fhir_ext/element_definition_spec.rb @@ -52,4 +52,19 @@ end end end + + describe '#type_paths' do + context 'with a single type' do + include_context 'single type' + it 'returns a single path option' do + expect(element_definition.type_paths).to eq(['Patient.deceased']) + end + end + context 'with a choice of types' do + include_context 'choice of type' + it 'returns a path for each type' do + expect(element_definition.type_paths).to eq(['Patient.deceasedBoolean', 'Patient.deceasedDateTime']) + end + end + end end \ No newline at end of file diff --git a/spec/unit/validation/retrieval_spec.rb b/spec/unit/validation/retrieval_spec.rb index add4cccdc..00e7194ed 100644 --- a/spec/unit/validation/retrieval_spec.rb +++ b/spec/unit/validation/retrieval_spec.rb @@ -89,4 +89,15 @@ end end end + + describe '#retrieve_element_with_fhirpath' do + let(:element_definition) {FHIR::ElementDefinition.new(id: id, path: path)} + context 'when retrieving the root element' do + let(:id) {'Patient'} + let(:path) {'Patient'} + it 'returns the root element (the resource)' do + expect(retrieval.retrieve_by_element_definition(resource, element_definition)).to eq({element_definition.id => resource}) + end + end + end end \ No newline at end of file From 108069ddf9e8b047675ce418e2a655655a18592b Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Mon, 12 Aug 2019 13:52:36 -0400 Subject: [PATCH 52/80] simplify type_code --- lib/fhir_models/fhir_ext/element_definition.rb | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/fhir_models/fhir_ext/element_definition.rb b/lib/fhir_models/fhir_ext/element_definition.rb index 93efea5ac..f0de60224 100644 --- a/lib/fhir_models/fhir_ext/element_definition.rb +++ b/lib/fhir_models/fhir_ext/element_definition.rb @@ -13,13 +13,9 @@ def type_code(element_path = nil) if type.one? type.first.code else - raise UnknownType, 'Need path in order to determine type' unless element_path + matching_code = matching_choice_type(element_path) - matching_code = type.find do |datatype| - /[^.]+$/.match(path.gsub('[x]', capitalize(datatype))).to_s == /[^.]+$/.match(element_path).to_s - end&.code - - raise UnknownType, "No matching types from #{type.flat_map(&:code)} for element at #{element_path}" if matching_code.nil? + raise(UnknownType, "No matching types from #{type.flat_map(&:code)} for element at #{element_path}") if matching_code.nil? matching_code end @@ -33,6 +29,13 @@ def type_paths end end + # Returns the type code for the matching type when a choice of types is present + private def matching_choice_type(element_path) + type.find do |datatype| + /[^.]+$/.match(path.gsub('[x]', capitalize(datatype))).to_s == /[^.]+$/.match(element_path).to_s + end&.code + end + private def capitalize(string) "#{string.code[0].capitalize}#{string.code[1..-1]}" end From 5046a59f41ac0f88ae60ffb6521eed424a919b19 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Mon, 12 Aug 2019 15:14:26 -0400 Subject: [PATCH 53/80] simplifying retrieving by fhirpath --- lib/fhir_models/validation/retrieval.rb | 73 ++++++++++++++++--------- 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/lib/fhir_models/validation/retrieval.rb b/lib/fhir_models/validation/retrieval.rb index d944dce0b..465ad52a9 100644 --- a/lib/fhir_models/validation/retrieval.rb +++ b/lib/fhir_models/validation/retrieval.rb @@ -7,40 +7,61 @@ module Retrieval # @param path [String] the path # @param resource [FHIR::Model] the resource from which the elements will be retrieved # @return [Hash] The desired elements - def self.retrieve_element_with_fhirpath(path, resource, indexed = true) - path_parts = path.split('.') + def self.retrieve_by_fhirpath(path, resource, indexed = true) + path_parts = path_parts(path) + last = path_parts.pop if path_parts.length != 1 && !indexed + + desired_elements = indexed_elements_by_fhirpath(path_parts.join('.'), resource) + + return desired_elements unless last + + # If we don't want to index the last element (useful for testing cardinality) + not_indexed = {} + desired_elements.each do |current_path, element| + not_indexed["#{current_path}.#{last}"] = retrieve_from_structure(last, element) + end + not_indexed + end + + # Returns an array of the path parts + # @param path [String] the path + # @return [Array] + def self.path_parts(path) + path.split('.') + end + + # Retrieve the specified element from the structure + # + # @param element [String] the element to be retrieved + # @param structure [FHIR::Model] the structure from which the element will be retrieved + def self.retrieve_from_structure(element, structure) + fixed_name = %w[class method resourceType].include?(element) ? "local_#{element}" : element + structure.send(fixed_name) if structure.is_a? FHIR::Model # FHIR Primitives are not modeled and will throw NoMethod Error + end + + # Returns the indexed elements given by the path + # + # @param fhirpath ['String'] the element path + # @resource [FHIR::Model] the resource from which the elements will be retrieved + # @return [Hash] + def self.indexed_elements_by_fhirpath(fhirpath, resource) + path_parts = path_parts(fhirpath) base_path = path_parts.shift fhir_path_elements = { base_path => resource } - last = path_parts.pop unless indexed - - desired_elements = path_parts.reduce(fhir_path_elements) do |parent, sub_path| + path_parts.reduce(fhir_path_elements) do |path_resource_map, sub_path| children = {} - parent.each do |parent_path, parent_value| - fixed_name = %w[class method resourceType].include?(sub_path) ? "local_#{sub_path}" : sub_path - elements = parent_value.send(fixed_name) if parent_value.is_a? FHIR::Model # FHIR Primitives are not modeled and will throw NoMethod Error - # More than one element where the FHIRPath needs indexing + path_resource_map.each do |path, element| + elements = retrieve_from_structure(sub_path, element) if elements.respond_to? :each_with_index elements.each_with_index do |indexed_element_value, indexed_element_path| - children["#{parent_path}.#{sub_path}[#{indexed_element_path}]"] = indexed_element_value unless blank?(indexed_element_value) + children["#{path}.#{sub_path}[#{indexed_element_path}]"] = indexed_element_value unless blank?(indexed_element_value) end - # Just One else - children["#{parent_path}.#{sub_path}"] = elements unless blank?(elements) + children["#{path}.#{sub_path}"] = elements unless blank?(elements) end end children end - - return desired_elements unless last - - # If we don't want to index the last element (useful for testing cardinality) - not_indexed = {} - desired_elements.each do |current_path, element| - fixed_name = %w[class method resourceType].include?(last) ? "local_#{last}" : last - elements = element.send(fixed_name) if element.is_a? FHIR::Model # FHIR Primitives are not modeled and will throw NoMethod Error - not_indexed["#{current_path}.#{last}"] = elements - end - not_indexed end # Retrieve the elements in the resource which are defined by the provided ElementDefinition @@ -58,7 +79,7 @@ def self.retrieve_by_element_definition(resource, element_definition, indexed: f elements = {} element_definition.type.each do |type| choice_type = type.code[0].upcase + type.code[1..-1] - type_element = retrieve_element_with_fhirpath(path.gsub('[x]', choice_type), resource, indexed) + type_element = retrieve_by_fhirpath(path.gsub('[x]', choice_type), resource, indexed) elements.merge!(type_element) unless blank?(type_element) end if normalized @@ -72,7 +93,7 @@ def self.retrieve_by_element_definition(resource, element_definition, indexed: f elements = choice_type_elements end else - elements = retrieve_element_with_fhirpath(path, resource, indexed) + elements = retrieve_by_fhirpath(path, resource, indexed) end # Handle Slices @@ -104,7 +125,7 @@ def self.blank?(obj) obj.respond_to?(:empty?) ? obj.empty? : obj.nil? end - private_class_method :blank? + private_class_method :blank?, :path_parts, :retrieve_from_structure, :indexed_elements_by_fhirpath end end end From 0072541f238353766a10af180015152ed8f16b8c Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Mon, 12 Aug 2019 15:46:02 -0400 Subject: [PATCH 54/80] breaking up retrieval... --- lib/fhir_models/validation/retrieval.rb | 135 +++++++++++++++--------- 1 file changed, 84 insertions(+), 51 deletions(-) diff --git a/lib/fhir_models/validation/retrieval.rb b/lib/fhir_models/validation/retrieval.rb index 465ad52a9..0756f83c1 100644 --- a/lib/fhir_models/validation/retrieval.rb +++ b/lib/fhir_models/validation/retrieval.rb @@ -52,15 +52,84 @@ def self.indexed_elements_by_fhirpath(fhirpath, resource) children = {} path_resource_map.each do |path, element| elements = retrieve_from_structure(sub_path, element) - if elements.respond_to? :each_with_index - elements.each_with_index do |indexed_element_value, indexed_element_path| - children["#{path}.#{sub_path}[#{indexed_element_path}]"] = indexed_element_value unless blank?(indexed_element_value) - end - else - children["#{path}.#{sub_path}"] = elements unless blank?(elements) + children["#{path}.#{sub_path}"] = elements unless blank?(elements) + end + index_elements(children) + end + end + + # Indexes the elements in the element map + # @param element_map [Hash] the map of paths to element + # @return [Hash] + def self.index_elements(element_map) + indexed_elements = {} + element_map.each do |path, elements| + if elements.respond_to? :each_with_index + elements.each_with_index do |indexed_element_value, indexed_element_path| + indexed_elements["#{path}[#{indexed_element_path}]"] = indexed_element_value unless blank?(indexed_element_value) end + else + indexed_elements[path.to_s] = elements unless blank?(elements) end - children + end + indexed_elements + end + + # Normalize type choice elements to [x] + # + # @param element_map [Hash] + # @param element_definition [FHIR::ElementDefinition] + # @return [Hash] + def self.normalize_elements(element_map, element_definition) + choice_type_elements = {} + element_map.each do |element_path, element| + renorm = element_path.rpartition('.').first + normalized_path = "#{renorm}.#{element_definition.path.split('.').last}" + choice_type_elements[normalized_path] ||= [] + choice_type_elements[normalized_path].push(element).compact! + end + choice_type_elements + end + + # Retrieve elements which have a choice of type + # @param resource [FHIR::Model] + # @param element_definition [FHIR::ElementDefinition] + # @param indexed [Boolean] If the elements should be returned individually or as a collection + # @param normalized [Boolean] If the elements with a choice of type should be normalized + # @return [Hash] + def self.retrieve_type_choice_elements(resource, element_definition, indexed: false, normalized: false) + elements = {} + element_definition.type_paths.each do |type_path| + type_element = retrieve_by_fhirpath(type_path, resource, indexed) + elements.merge!(type_element) unless blank?(type_element) + end + + elements = normalize_elements(elements, element_definition) if normalized + elements + end + + # Retrieve only the matching slices from the elements + # @param element_definition [FHIR::ElementDefinition] + # @param indexed [Boolean] If the elements should be returned individually or as a collection + # @return [Hash] + def self.handle_slices(element_map, element_definition, indexed: false) + # Grab Extension slices + return {} unless element_definition.type.first.code == 'Extension' + + # Only select the elements which match the slice profile. + if indexed + # Elements are already indexed + element_map.select! do |_k, element| + element.url == element_definition.type.first.profile.first + end + else + sliced_elements = {} + element_map.each do |element_path, element| + sliced_elements[element_path] = element.select do |extension| + extension.url == element_definition.type.first.profile.first + end + end + sliced_elements end end @@ -74,50 +143,13 @@ def self.retrieve_by_element_definition(resource, element_definition, indexed: f # Check if we were provided a path that includes extensions (like in the ElementDefinition id versus the path) path = element_definition.path - # Check for multiple choice types - if element_definition.choice_type? - elements = {} - element_definition.type.each do |type| - choice_type = type.code[0].upcase + type.code[1..-1] - type_element = retrieve_by_fhirpath(path.gsub('[x]', choice_type), resource, indexed) - elements.merge!(type_element) unless blank?(type_element) - end - if normalized - choice_type_elements = {} - elements.each do |element_path, element| - renorm = element_path.rpartition('.').first - normalized_path = "#{renorm}.#{element_definition.path.split('.').last}" - choice_type_elements[normalized_path] ||= [] - choice_type_elements[normalized_path].push(element).compact! - end - elements = choice_type_elements - end - else - elements = retrieve_by_fhirpath(path, resource, indexed) - end + elements = if element_definition.choice_type? + retrieve_type_choice_elements(resource, element_definition, indexed: indexed, normalized: normalized) + else + retrieve_by_fhirpath(path, resource, indexed) + end - # Handle Slices - if element_definition.sliceName - # TODO: Need to be able to get other types of slices - # Grab Extension slices - return {} unless element_definition.type.first.code == 'Extension' - - # Only select the elements which match the slice profile. - if indexed - # Elements are already indexed - elements.select! do |_k, element| - element.url == element_definition.type.first.profile.first - end - else - sliced_elements = {} - elements.each do |element_path, element| - sliced_elements[element_path] = element.select do |extension| - extension.url == element_definition.type.first.profile.first - end - end - elements = sliced_elements - end - end + elements = handle_slices(elements, element_definition, indexed: indexed, normalized: normalized) if element_definition.sliceName elements end @@ -125,7 +157,8 @@ def self.blank?(obj) obj.respond_to?(:empty?) ? obj.empty? : obj.nil? end - private_class_method :blank?, :path_parts, :retrieve_from_structure, :indexed_elements_by_fhirpath + private_class_method :blank?, :path_parts, :retrieve_from_structure, :indexed_elements_by_fhirpath, + :index_elements, :normalize_elements, :retrieve_type_choice_elements, :handle_slices end end end From 40e7a16069948b27684b872a298b45ee534f9c88 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Mon, 12 Aug 2019 16:05:25 -0400 Subject: [PATCH 55/80] breakup indexing of elements --- lib/fhir_models/validation/retrieval.rb | 43 ++++++++++++++++++------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/lib/fhir_models/validation/retrieval.rb b/lib/fhir_models/validation/retrieval.rb index 0756f83c1..84eca1c78 100644 --- a/lib/fhir_models/validation/retrieval.rb +++ b/lib/fhir_models/validation/retrieval.rb @@ -64,13 +64,34 @@ def self.indexed_elements_by_fhirpath(fhirpath, resource) def self.index_elements(element_map) indexed_elements = {} element_map.each do |path, elements| - if elements.respond_to? :each_with_index - elements.each_with_index do |indexed_element_value, indexed_element_path| - indexed_elements["#{path}[#{indexed_element_path}]"] = indexed_element_value unless blank?(indexed_element_value) - end - else - indexed_elements[path.to_s] = elements unless blank?(elements) - end + indexed_elements.merge!(index_elements_for_path(elements, path)) + end + indexed_elements + end + + # Index enumerable element collections + # + # @param elements [Array] + # @param path [String] the path to the element collection + # @return [Hash] + def self.index_element_collection(elements, path) + indexed_elements = {} + elements.each_with_index do |indexed_element_value, indexed_element_path| + indexed_elements["#{path}[#{indexed_element_path}]"] = indexed_element_value unless blank?(indexed_element_value) + end + indexed_elements + end + + # Index elements on a specific path + # @param elements + # @param path [String] the path to the element collection + # @return [Hash] + def self.index_elements_for_path(elements, path) + indexed_elements = {} + if elements.respond_to? :each_with_index + indexed_elements = index_element_collection(elements, path) + else + indexed_elements[path.to_s] = elements unless blank?(elements) end indexed_elements end @@ -83,8 +104,8 @@ def self.index_elements(element_map) def self.normalize_elements(element_map, element_definition) choice_type_elements = {} element_map.each do |element_path, element| - renorm = element_path.rpartition('.').first - normalized_path = "#{renorm}.#{element_definition.path.split('.').last}" + base_path = element_path.rpartition('.').first + normalized_path = "#{base_path}.#{element_definition.path.split('.').last}" choice_type_elements[normalized_path] ||= [] choice_type_elements[normalized_path].push(element).compact! end @@ -118,7 +139,6 @@ def self.handle_slices(element_map, element_definition, indexed: false) # Only select the elements which match the slice profile. if indexed - # Elements are already indexed element_map.select! do |_k, element| element.url == element_definition.type.first.profile.first end @@ -140,7 +160,6 @@ def self.handle_slices(element_map, element_definition, indexed: false) # @param indexed [Boolean] If the elements should be returned individually or as a collection # @param normalized [Boolean] If the elements with a choice of type should be normalized def self.retrieve_by_element_definition(resource, element_definition, indexed: false, normalized: false) - # Check if we were provided a path that includes extensions (like in the ElementDefinition id versus the path) path = element_definition.path elements = if element_definition.choice_type? @@ -149,7 +168,7 @@ def self.retrieve_by_element_definition(resource, element_definition, indexed: f retrieve_by_fhirpath(path, resource, indexed) end - elements = handle_slices(elements, element_definition, indexed: indexed, normalized: normalized) if element_definition.sliceName + elements = handle_slices(elements, element_definition, indexed: indexed) if element_definition.sliceName elements end From 14484696fd76fe6c8d276199106aabcc5f0c8e5a Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Tue, 13 Aug 2019 09:45:03 -0400 Subject: [PATCH 56/80] break out parts of code checking. rename retrieve_type_choice_elements to retrieve_choice_type_elements --- .../element_validator/code_validator.rb | 18 ++++++++++++------ lib/fhir_models/validation/retrieval.rb | 6 +++--- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/fhir_models/validation/element_validator/code_validator.rb b/lib/fhir_models/validation/element_validator/code_validator.rb index e337bcfa0..2d99ea0b8 100644 --- a/lib/fhir_models/validation/element_validator/code_validator.rb +++ b/lib/fhir_models/validation/element_validator/code_validator.rb @@ -61,18 +61,24 @@ def validate required_binding? ? :fail : :warn end + # Check that both a code and system are present in the coded element + # @param coding [#code & #system] + # @return [Array] + private def check_for_code_and_system(coding) + check_results = [] + check_results.push(new_result(:warn, "#{path}: missing code")) if coding.code.nil? + check_results.push(new_result(:warn, "#{path}: missing system")) if coding.system.nil? + check_results + end + # Check the code for an individually element # # @param coding [#code & #system] the coded element containing a code and system # @param valueset_uri [String] the valueset uri private def check_code(coding, valueset_uri) - check_results = [] # Can't validate if both code and system are not given - if coding.code.nil? || coding.system.nil? - check_results.push(new_result(:warn, "#{path}: missing code")) if coding.code.nil? - check_results.push(new_result(:warn, "#{path}: missing system")) if coding.system.nil? - return check_results - end + check_results = check_for_code_and_system(coding) + return check_results unless check_results.empty? # ValueSet Validation check_results.push(validate_code(coding, valueset_uri, fail_level)) diff --git a/lib/fhir_models/validation/retrieval.rb b/lib/fhir_models/validation/retrieval.rb index 84eca1c78..e3032c405 100644 --- a/lib/fhir_models/validation/retrieval.rb +++ b/lib/fhir_models/validation/retrieval.rb @@ -118,7 +118,7 @@ def self.normalize_elements(element_map, element_definition) # @param indexed [Boolean] If the elements should be returned individually or as a collection # @param normalized [Boolean] If the elements with a choice of type should be normalized # @return [Hash] - def self.retrieve_type_choice_elements(resource, element_definition, indexed: false, normalized: false) + def self.retrieve_choice_type_elements(resource, element_definition, indexed: false, normalized: false) elements = {} element_definition.type_paths.each do |type_path| type_element = retrieve_by_fhirpath(type_path, resource, indexed) @@ -163,7 +163,7 @@ def self.retrieve_by_element_definition(resource, element_definition, indexed: f path = element_definition.path elements = if element_definition.choice_type? - retrieve_type_choice_elements(resource, element_definition, indexed: indexed, normalized: normalized) + retrieve_choice_type_elements(resource, element_definition, indexed: indexed, normalized: normalized) else retrieve_by_fhirpath(path, resource, indexed) end @@ -177,7 +177,7 @@ def self.blank?(obj) end private_class_method :blank?, :path_parts, :retrieve_from_structure, :indexed_elements_by_fhirpath, - :index_elements, :normalize_elements, :retrieve_type_choice_elements, :handle_slices + :index_elements, :normalize_elements, :retrieve_choice_type_elements, :handle_slices end end end From 1fdcbd4ab84b558a28e6d0b70267b6363542dbf6 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Tue, 13 Aug 2019 10:03:11 -0400 Subject: [PATCH 57/80] more refactoring --- .rubocop.yml | 2 ++ .../element_validator/data_type_validator.rb | 4 ++-- .../validation/structure_validation_result.rb | 14 ++++++++++---- lib/fhir_models/validation/structure_validator.rb | 4 ++-- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 9136451e8..7fecdf660 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -17,3 +17,5 @@ Style/FrozenStringLiteralComment: inherit_mode: merge: - Exclude +Naming/RescuedExceptionsVariableName: + PreferredName: 'e' diff --git a/lib/fhir_models/validation/element_validator/data_type_validator.rb b/lib/fhir_models/validation/element_validator/data_type_validator.rb index 8c456b52f..03758f514 100644 --- a/lib/fhir_models/validation/element_validator/data_type_validator.rb +++ b/lib/fhir_models/validation/element_validator/data_type_validator.rb @@ -34,11 +34,11 @@ def self.validate_element(element, element_definition, path) begin type_code = element_definition.type_code(path) type_def = FHIR::Definitions.definition(type_code) - rescue FHIR::UnknownType => ex + rescue FHIR::UnknownType => e return FHIR::ValidationResult.new(element_definition: element_definition, validation_type: :datatype, result: :warn, - text: ex.message, + text: e.message, element_path: path || element_definition.path) end diff --git a/lib/fhir_models/validation/structure_validation_result.rb b/lib/fhir_models/validation/structure_validation_result.rb index 583310dcc..13a126940 100644 --- a/lib/fhir_models/validation/structure_validation_result.rb +++ b/lib/fhir_models/validation/structure_validation_result.rb @@ -12,11 +12,17 @@ def all_results all_validation_results(@validation_hash) end + private def slice_results(element_node) + element_node[:slices].empty? ? [] : all_validation_results(element_node[:slices]) + end + + private def sub_element_results(element_node) + element_node[:path].empty? ? [] : all_validation_results(element_node[:path]) + end + private def all_validation_results(hierarchy) - hierarchy.flat_map do |_, v| - slice_results = v[:slices].empty? ? [] : all_validation_results(v[:slices]) - sub_element_results = v[:path].empty? ? [] : all_validation_results(v[:path]) - v[:results].concat(slice_results, sub_element_results) + hierarchy.flat_map do |_, element_node| + element_node[:results].concat(slice_results(element_node), sub_element_results(element_node)) end end end diff --git a/lib/fhir_models/validation/structure_validator.rb b/lib/fhir_models/validation/structure_validator.rb index a2882aec9..3ab2f88ab 100644 --- a/lib/fhir_models/validation/structure_validator.rb +++ b/lib/fhir_models/validation/structure_validator.rb @@ -54,9 +54,9 @@ def add_default_element_validators # # @param [Array] List of Element Definitions # @return [Hash] The ElementDefinition hierarchy - private def build_hierarchy(elem_def_list) + private def build_hierarchy(element_definitions) hierarchy = {} - elem_def_list.each do |element| + element_definitions.each do |element| # Separate path and slices into an array of keys slices = element.id.split(':') path = slices.shift.split('.') From 631e4890485fca83b21d982260d04c8c1aa376bd Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Tue, 13 Aug 2019 11:01:33 -0400 Subject: [PATCH 58/80] breaking up the build_hierarchy method --- .../validation/structure_validator.rb | 51 ++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/lib/fhir_models/validation/structure_validator.rb b/lib/fhir_models/validation/structure_validator.rb index 3ab2f88ab..2239c94b0 100644 --- a/lib/fhir_models/validation/structure_validator.rb +++ b/lib/fhir_models/validation/structure_validator.rb @@ -50,6 +50,30 @@ def add_default_element_validators Validation::MaxLengthValidator]) end + private def parse_element_id(element_id) + element_id_parts = {} + element_id_parts[:slices] = element_id.split(':') + element_id_parts[:path] = element_id_parts[:slices].shift.split('.') + element_id_parts[:slices] = element_id_parts[:slices].pop&.split('/') + element_id_parts[:last_path] = element_id_parts[:slices].nil? ? element_id_parts[:path].pop : element_id_parts[:slices].pop + element_id_parts + end + + private def add_element_path_to_hierarchy(hierarchy, path) + path.reduce(hierarchy) do |hierarchy_memo, path_path| + hierarchy_memo[path_path] ||= { elementDefinition: nil, path: {}, slices: {} } + hierarchy_memo[path_path][:path] + end + end + + private def add_element_slices_to_hierarchy(hierarchy, path, slices) + path_down = path.zip(Array.new(path.length - 1, :path)).push(:slices).flatten.compact + slices.inject(hierarchy.dig(*path_down)) do |memo, k| + memo[k] ||= { elementDefinition: nil, path: {}, slices: {} } + memo[k][:slices] + end + end + # Build a hierarchy from a list of ElementDefinitions # # @param [Array] List of Element Definitions @@ -57,29 +81,10 @@ def add_default_element_validators private def build_hierarchy(element_definitions) hierarchy = {} element_definitions.each do |element| - # Separate path and slices into an array of keys - slices = element.id.split(':') - path = slices.shift.split('.') - slices = slices.pop&.split('/') - last_path = slices.nil? ? path.pop : slices.pop - - # Build the hierarchy - thing = path.inject(hierarchy) do |memo, k| - memo[k] ||= { elementDefinition: nil, path: {}, slices: {} } - memo[k][:path] - end - - # If there are slices - unless slices.nil? - path_down = path.zip(Array.new(path.length - 1, :path)).push(:slices).flatten.compact - thing = slices.inject(hierarchy.dig(*path_down)) do |memo, k| - memo[k] ||= { elementDefinition: nil, path: {}, slices: {} } - memo[k][:slices] - end - end - - # If there are no slices - thing[last_path] = { elementDefinition: element, path: {}, slices: {} } + element_id_parts = parse_element_id(element.id) + current_node = add_element_path_to_hierarchy(hierarchy, element_id_parts[:path]) + current_node = add_element_slices_to_hierarchy(hierarchy, element_id_parts[:path], element_id_parts[:slices]) unless element_id_parts[:slices].nil? + current_node[element_id_parts[:last_path]] = { elementDefinition: element, path: {}, slices: {} } end hierarchy end From c048b665257597d579864bb4ba796f570d64edbd Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Tue, 13 Aug 2019 13:16:00 -0400 Subject: [PATCH 59/80] breaking out parts of validate_hierarchy --- .../validation/structure_validator.rb | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/lib/fhir_models/validation/structure_validator.rb b/lib/fhir_models/validation/structure_validator.rb index 2239c94b0..2ee1de0a0 100644 --- a/lib/fhir_models/validation/structure_validator.rb +++ b/lib/fhir_models/validation/structure_validator.rb @@ -5,7 +5,6 @@ class StructureValidator @vs_validators = {} attr_accessor :all_results - attr_accessor :show_skipped attr_reader :element_validators # Create a StructureValidator from a FHIR::StructureDefinition @@ -14,7 +13,6 @@ class StructureValidator # def initialize(profile, use_default_element_validators: true) @profile = profile - @show_skipped = true @element_validators = Set.new add_default_element_validators if use_default_element_validators end @@ -99,25 +97,23 @@ def add_default_element_validators @snapshot_hierarchy end + private def elements_exist_in_resource(resource, element_definition) + elements = resource.retrieve_elements_by_definition(element_definition) + !blank?(elements.values.flatten.compact) + end + private def validate_hierarchy(resource, hierarchy) - # Validate the element hierarchy[:results] ||= [] element_definition = hierarchy[:elementDefinition] - # Get the Results + # Get the results from each element validator results = @element_validators.flat_map { |validator| validator.validate(resource, element_definition) } - results.compact! - results.each { |res| res.profile ||= @profile.url } + results.compact!.each { |res| res.profile ||= @profile.url } # Save the validation results - hierarchy[:results].push(*results) - - # Check to see if there are any valid elements to determine if we need to check the subelements - elements = resource.retrieve_elements_by_definition(element_definition) - element_exists = !blank?(elements.values.flatten.compact) + hierarchy[:results].concat(results) - # If the element doesn't exist we don't need to check its subelements unless we are instructed to by showskipped - return unless @show_skipped || element_exists + return unless elements_exist_in_resource(resource, element_definition) # Validate the subpath elements hierarchy[:path].values.each { |v| validate_hierarchy(resource, v) } From 18a04aaf211a11b4715ba135adf6e30a506a188d Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Tue, 13 Aug 2019 13:21:35 -0400 Subject: [PATCH 60/80] add method for executing element validators --- lib/fhir_models/validation/structure_validator.rb | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/fhir_models/validation/structure_validator.rb b/lib/fhir_models/validation/structure_validator.rb index 2ee1de0a0..88287269e 100644 --- a/lib/fhir_models/validation/structure_validator.rb +++ b/lib/fhir_models/validation/structure_validator.rb @@ -102,16 +102,17 @@ def add_default_element_validators !blank?(elements.values.flatten.compact) end + private def validate_against_element_validators(resource, element_definition) + @element_validators.flat_map { |validator| validator.validate(resource, element_definition) } + .compact! + .each { |res| res.profile ||= @profile.url } + end + private def validate_hierarchy(resource, hierarchy) hierarchy[:results] ||= [] element_definition = hierarchy[:elementDefinition] - # Get the results from each element validator - results = @element_validators.flat_map { |validator| validator.validate(resource, element_definition) } - results.compact!.each { |res| res.profile ||= @profile.url } - - # Save the validation results - hierarchy[:results].concat(results) + hierarchy[:results].concat(validate_against_element_validators(resource, element_definition)) return unless elements_exist_in_resource(resource, element_definition) From 175feb5f6a80198aa93670c4d2581f5ba52838da Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Tue, 13 Aug 2019 13:30:14 -0400 Subject: [PATCH 61/80] more informative variable name --- lib/fhir_models/validation/structure_validator.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/fhir_models/validation/structure_validator.rb b/lib/fhir_models/validation/structure_validator.rb index 88287269e..f03dce291 100644 --- a/lib/fhir_models/validation/structure_validator.rb +++ b/lib/fhir_models/validation/structure_validator.rb @@ -116,11 +116,8 @@ def add_default_element_validators return unless elements_exist_in_resource(resource, element_definition) - # Validate the subpath elements - hierarchy[:path].values.each { |v| validate_hierarchy(resource, v) } - - # Validate the slices elements - hierarchy[:slices].values.each { |v| validate_hierarchy(resource, v) } + hierarchy[:path].values.each { |sub_hierarchy| validate_hierarchy(resource, sub_hierarchy) } + hierarchy[:slices].values.each { |sub_hierarchy| validate_hierarchy(resource, sub_hierarchy) } end private def blank?(obj) From 15ff1e991d2cb046e62b382e1912496b1bc69a23 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Tue, 13 Aug 2019 13:33:23 -0400 Subject: [PATCH 62/80] remove unnecessary spec --- spec/lib/fhir_models/bootstrap/my_example_spec.rb | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 spec/lib/fhir_models/bootstrap/my_example_spec.rb diff --git a/spec/lib/fhir_models/bootstrap/my_example_spec.rb b/spec/lib/fhir_models/bootstrap/my_example_spec.rb deleted file mode 100644 index 33f9ab813..000000000 --- a/spec/lib/fhir_models/bootstrap/my_example_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -describe 'FHIR::JSON' do - - it 'allows FHIR::Models to be represented as JSON' do - patient = FHIR::Patient.new - expect(patient.to_json).to eq("{\n \"resourceType\": \"Patient\"\n}") - end -end \ No newline at end of file From ff1536f7f1aaad31c022d5d3b0a26d2951820b5c Mon Sep 17 00:00:00 2001 From: Reece Adamson <41651655+radamson@users.noreply.github.com> Date: Tue, 13 Aug 2019 13:34:47 -0400 Subject: [PATCH 63/80] Update spec/unit/validation/retrieval_spec.rb Co-Authored-By: Stephen MacVicar --- spec/unit/validation/retrieval_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/unit/validation/retrieval_spec.rb b/spec/unit/validation/retrieval_spec.rb index 00e7194ed..48d18a2a3 100644 --- a/spec/unit/validation/retrieval_spec.rb +++ b/spec/unit/validation/retrieval_spec.rb @@ -10,7 +10,7 @@ end describe '#retrieve_by_element_definition' do - it 'returns the root element or a resource' do + it 'returns the root element of a resource' do element_definition = FHIR::ElementDefinition.new(id: 'Patient', path: 'Patient') expect(retrieval.retrieve_by_element_definition(resource, element_definition)).to eq({element_definition.id => resource}) end @@ -100,4 +100,4 @@ end end end -end \ No newline at end of file +end From 47c9c45169cb535eea5b588095163b63b920acd2 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Tue, 13 Aug 2019 13:51:29 -0400 Subject: [PATCH 64/80] method for empty hierarchy node --- lib/fhir_models/validation/structure_validator.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/fhir_models/validation/structure_validator.rb b/lib/fhir_models/validation/structure_validator.rb index f03dce291..0aeb00ea1 100644 --- a/lib/fhir_models/validation/structure_validator.rb +++ b/lib/fhir_models/validation/structure_validator.rb @@ -59,15 +59,19 @@ def add_default_element_validators private def add_element_path_to_hierarchy(hierarchy, path) path.reduce(hierarchy) do |hierarchy_memo, path_path| - hierarchy_memo[path_path] ||= { elementDefinition: nil, path: {}, slices: {} } + hierarchy_memo[path_path] ||= hierarchy_node hierarchy_memo[path_path][:path] end end + private def hierarchy_node(element_definition = nil) + { elementDefinition: element_definition, path: {}, slices: {}, results: [] } + end + private def add_element_slices_to_hierarchy(hierarchy, path, slices) path_down = path.zip(Array.new(path.length - 1, :path)).push(:slices).flatten.compact slices.inject(hierarchy.dig(*path_down)) do |memo, k| - memo[k] ||= { elementDefinition: nil, path: {}, slices: {} } + memo[k] ||= hierarchy_node memo[k][:slices] end end @@ -82,7 +86,7 @@ def add_default_element_validators element_id_parts = parse_element_id(element.id) current_node = add_element_path_to_hierarchy(hierarchy, element_id_parts[:path]) current_node = add_element_slices_to_hierarchy(hierarchy, element_id_parts[:path], element_id_parts[:slices]) unless element_id_parts[:slices].nil? - current_node[element_id_parts[:last_path]] = { elementDefinition: element, path: {}, slices: {} } + current_node[element_id_parts[:last_path]] = hierarchy_node(element) end hierarchy end From 68577ab5b78fb7df3d20c0aed7e4ef03f4e76539 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Tue, 13 Aug 2019 17:06:10 -0400 Subject: [PATCH 65/80] dry out cardinality tests --- .../cardinality_validator_spec.rb | 147 +++++------------- 1 file changed, 38 insertions(+), 109 deletions(-) diff --git a/spec/unit/validation/element_validator/cardinality_validator_spec.rb b/spec/unit/validation/element_validator/cardinality_validator_spec.rb index f0dbdb7c7..90f25ef61 100644 --- a/spec/unit/validation/element_validator/cardinality_validator_spec.rb +++ b/spec/unit/validation/element_validator/cardinality_validator_spec.rb @@ -1,9 +1,23 @@ describe FHIR::Validation::CardinalityValidator do let(:validator) { FHIR::Validation::CardinalityValidator } + let(:resource) do - FHIR::Patient.new(id: 2) + FHIR::Patient.new + end + + shared_context 'a single element' do + let(:id) {'the id'} + end + + shared_context 'multiple elements' do + let(:id) { [1,2] } end + + shared_context 'no elements' do + let(:id) { nil } + end + describe '#validate' do let(:element_definition) do FHIR::ElementDefinition.new(min: 0, max: 1, @@ -16,115 +30,30 @@ expect(results).to be_nil end - context 'when the cardinality is 0..1' do - let(:element_definition) do FHIR::ElementDefinition.new(min: 0, - max: 1, - id: 'Patient.id', - path: 'Patient.id') - end - it 'passes when the element has a single value' do - results = validator.validate(resource, element_definition) - expect(results.size).to eq(1) - expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(result: :pass)) - end - it 'fails when more than one element is present' do - resource.id = [1,2] - results = validator.validate(resource, element_definition) - expect(results.size).to eq(1) - expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(result: :fail)) - end - it 'passes when no elements are present' do - resource.id = nil - results = validator.validate(resource, element_definition) - expect(results.size).to eq(1) - expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(result: :pass)) - end - end + scenarios = {'0..1' => {'a single element' => :pass, 'no elements' => :pass, 'multiple elements' => :fail}, + '0..*' => {'a single element' => :pass, 'no elements' => :pass, 'multiple elements' => :pass}, + '1..1' => {'a single element' => :pass, 'no elements' => :fail, 'multiple elements' => :fail}, + '1..*' => {'a single element' => :pass, 'no elements' => :fail, 'multiple elements' => :pass}} - context 'when the cardinality is 0..*' do - let(:element_definition) do FHIR::ElementDefinition.new(min: 0, - max: '*', - id: 'Patient.id', - path: 'Patient.id') - end - it 'passes when the element has a single value' do - results = validator.validate(resource, element_definition) - expect(results.size).to eq(1) - expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(result: :pass)) - end - it 'passes when more than one element is present' do - resource.id = [1,2] - results = validator.validate(resource, element_definition) - expect(results.size).to eq(1) - expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(result: :pass)) - end - it 'passes when no elements are present' do - resource.id = nil - results = validator.validate(resource, element_definition) - expect(results.size).to eq(1) - expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(result: :pass)) - end - end - - context 'when the cardinality is 1..1' do - let(:element_definition) do FHIR::ElementDefinition.new(min: 1, - max: 1, - id: 'Patient.id', - path: 'Patient.id') - end - it 'passes when the element has a single value' do - results = validator.validate(resource, element_definition) - expect(results.size).to eq(1) - expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(result: :pass)) - end - it 'fails when more than one element is present' do - resource.id = [1,2] - results = validator.validate(resource, element_definition) - expect(results.size).to eq(1) - expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(result: :fail)) - end - it 'fails when no elements are present' do - resource.id = nil - results = validator.validate(resource, element_definition) - expect(results.size).to eq(1) - expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(result: :fail)) - end - end - - context 'when the cardinality is 1..*' do - let(:element_definition) do FHIR::ElementDefinition.new(min: 1, - max: '*', - id: 'Patient.id', - path: 'Patient.id') - end - it 'passes when the element has a single value' do - results = validator.validate(resource, element_definition) - expect(results.size).to eq(1) - expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(result: :pass)) - end - it 'passes when more than one element is present' do - resource.id = [1,2] - results = validator.validate(resource, element_definition) - expect(results.size).to eq(1) - expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(result: :pass)) - end - it 'fails when no elements are present' do - resource.id = nil - results = validator.validate(resource, element_definition) - expect(results.size).to eq(1) - expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(result: :fail)) + scenarios.each do |cardinality, number_of_elements_scenario| + context "when the cardinality is #{cardinality[0]}..#{cardinality[-1]}" do + let(:element_definition) do FHIR::ElementDefinition.new(min: cardinality[0], + max: cardinality[-1], + id: 'Patient.id', + path: 'Patient.id') + end + number_of_elements_scenario.each do |number_of_elements, expected_result| + context "when the element contains #{number_of_elements}" do + include_context number_of_elements + it "#{expected_result} when the element contains #{number_of_elements}" do + resource.id = id + results = validator.validate(resource, element_definition) + expect(results.size).to eq(1) + expect(results).to all(have_attributes(validation_type: :cardinality)) + expect(results).to all(have_attributes(result: expected_result)) + end + end + end end end end From 9060560593e3bc29e45acc76eaf2f1476bac9508 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Tue, 13 Aug 2019 17:42:36 -0400 Subject: [PATCH 66/80] enable rubocop rspec checking --- .rubocop.yml | 4 +++- Gemfile.lock | 3 +++ fhir_models.gemspec | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.rubocop.yml b/.rubocop.yml index 7fecdf660..ae408fa08 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,9 +1,9 @@ +require: rubocop-rspec inherit_from: .rubocop_todo.yml AllCops: TargetRubyVersion: 2.4 Exclude: - 'test/**/*' - - 'spec/**/*' - 'lib/fhir_models/fhir/**/*' - 'lib/fhir_models/fluentpath/evaluate.rb' - 'lib/**/*.rake' @@ -19,3 +19,5 @@ inherit_mode: - Exclude Naming/RescuedExceptionsVariableName: PreferredName: 'e' +RSpec/FilePath: + Enabled: false \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 3d5153bb3..f1a62a2ed 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -100,6 +100,8 @@ GEM rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 1.6) + rubocop-rspec (1.35.0) + rubocop (>= 0.60.0) ruby-progressbar (1.10.0) ruby_dep (1.5.0) shellany (0.0.1) @@ -130,6 +132,7 @@ DEPENDENCIES rake rspec rubocop (= 0.67) + rubocop-rspec simplecov test-unit diff --git a/fhir_models.gemspec b/fhir_models.gemspec index a144a7edd..4423dce50 100644 --- a/fhir_models.gemspec +++ b/fhir_models.gemspec @@ -34,6 +34,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'simplecov' spec.add_development_dependency 'nokogiri-diff' spec.add_development_dependency 'rubocop', '0.67' + spec.add_development_dependency 'rubocop-rspec' spec.add_development_dependency 'codeclimate-test-reporter' spec.add_development_dependency 'guard-rspec' spec.add_development_dependency 'guard-test' From 658fa4167ced6d0cc73eb6a23889bfc1b02dfb6b Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Tue, 13 Aug 2019 17:48:36 -0400 Subject: [PATCH 67/80] rubocop auto correct spec helper --- spec/spec_helper.rb | 104 ++++++++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 53 deletions(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6ea31f03b..4dbf006b2 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -53,57 +53,55 @@ # triggering implicit auto-inclusion in groups with matching metadata. config.shared_context_metadata_behavior = :apply_to_host_groups -# The settings below are suggested to provide a good initial experience -# with RSpec, but feel free to customize to your heart's content. -=begin - # This allows you to limit a spec run to individual examples or groups - # you care about by tagging them with `:focus` metadata. When nothing - # is tagged with `:focus`, all examples get run. RSpec also provides - # aliases for `it`, `describe`, and `context` that include `:focus` - # metadata: `fit`, `fdescribe` and `fcontext`, respectively. - config.filter_run_when_matching :focus - - # Allows RSpec to persist some state between runs in order to support - # the `--only-failures` and `--next-failure` CLI options. We recommend - # you configure your source control system to ignore this file. - config.example_status_persistence_file_path = "spec/examples.txt" - - # Limits the available syntax to the non-monkey patched syntax that is - # recommended. For more details, see: - # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ - # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ - # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode - config.disable_monkey_patching! - - # This setting enables warnings. It's recommended, but in some cases may - # be too noisy due to issues in dependencies. - config.warnings = true - - # Many RSpec users commonly either run the entire suite or an individual - # file, and it's useful to allow more verbose output when running an - # individual spec file. - if config.files_to_run.one? - # Use the documentation formatter for detailed output, - # unless a formatter has already been configured - # (e.g. via a command-line flag). - config.default_formatter = 'doc' - end - - # Print the 10 slowest examples and example groups at the - # end of the spec run, to help surface which specs are running - # particularly slow. - config.profile_examples = 10 - - # Run specs in random order to surface order dependencies. If you find an - # order dependency and want to debug it, you can fix the order by providing - # the seed, which is printed after each run. - # --seed 1234 - config.order = :random - - # Seed global randomization in this process using the `--seed` CLI option. - # Setting this allows you to use `--seed` to deterministically reproduce - # test failures related to randomization by passing the same `--seed` value - # as the one that triggered the failure. - Kernel.srand config.seed -=end + # The settings below are suggested to provide a good initial experience + # with RSpec, but feel free to customize to your heart's content. + # # This allows you to limit a spec run to individual examples or groups + # # you care about by tagging them with `:focus` metadata. When nothing + # # is tagged with `:focus`, all examples get run. RSpec also provides + # # aliases for `it`, `describe`, and `context` that include `:focus` + # # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + # config.filter_run_when_matching :focus + # + # # Allows RSpec to persist some state between runs in order to support + # # the `--only-failures` and `--next-failure` CLI options. We recommend + # # you configure your source control system to ignore this file. + # config.example_status_persistence_file_path = "spec/examples.txt" + # + # # Limits the available syntax to the non-monkey patched syntax that is + # # recommended. For more details, see: + # # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + # config.disable_monkey_patching! + # + # # This setting enables warnings. It's recommended, but in some cases may + # # be too noisy due to issues in dependencies. + # config.warnings = true + # + # # Many RSpec users commonly either run the entire suite or an individual + # # file, and it's useful to allow more verbose output when running an + # # individual spec file. + # if config.files_to_run.one? + # # Use the documentation formatter for detailed output, + # # unless a formatter has already been configured + # # (e.g. via a command-line flag). + # config.default_formatter = 'doc' + # end + # + # # Print the 10 slowest examples and example groups at the + # # end of the spec run, to help surface which specs are running + # # particularly slow. + # config.profile_examples = 10 + # + # # Run specs in random order to surface order dependencies. If you find an + # # order dependency and want to debug it, you can fix the order by providing + # # the seed, which is printed after each run. + # # --seed 1234 + # config.order = :random + # + # # Seed global randomization in this process using the `--seed` CLI option. + # # Setting this allows you to use `--seed` to deterministically reproduce + # # test failures related to randomization by passing the same `--seed` value + # # as the one that triggered the failure. + # Kernel.srand config.seed end From 7ce7156cad45bb573f898b7ee069a3b3b318fa80 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Tue, 13 Aug 2019 17:49:06 -0400 Subject: [PATCH 68/80] clean up cardinality validator tests --- .../cardinality_validator_spec.rb | 83 ++++++++++--------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/spec/unit/validation/element_validator/cardinality_validator_spec.rb b/spec/unit/validation/element_validator/cardinality_validator_spec.rb index 90f25ef61..d793250da 100644 --- a/spec/unit/validation/element_validator/cardinality_validator_spec.rb +++ b/spec/unit/validation/element_validator/cardinality_validator_spec.rb @@ -1,28 +1,16 @@ describe FHIR::Validation::CardinalityValidator do - - let(:validator) { FHIR::Validation::CardinalityValidator } + let(:validator) { described_class } let(:resource) do FHIR::Patient.new end - - shared_context 'a single element' do - let(:id) {'the id'} - end - - shared_context 'multiple elements' do - let(:id) { [1,2] } - end - - shared_context 'no elements' do - let(:id) { nil } - end describe '#validate' do - let(:element_definition) do FHIR::ElementDefinition.new(min: 0, - max: 1, - id: 'Patient', - path: 'Patient') + let(:element_definition) do + FHIR::ElementDefinition.new(min: 0, + max: 1, + id: 'Patient', + path: 'Patient') end it 'skips cardinality on the root element' do @@ -30,31 +18,50 @@ expect(results).to be_nil end - scenarios = {'0..1' => {'a single element' => :pass, 'no elements' => :pass, 'multiple elements' => :fail}, - '0..*' => {'a single element' => :pass, 'no elements' => :pass, 'multiple elements' => :pass}, - '1..1' => {'a single element' => :pass, 'no elements' => :fail, 'multiple elements' => :fail}, - '1..*' => {'a single element' => :pass, 'no elements' => :fail, 'multiple elements' => :pass}} + scenarios = { '0..1' => { 'with a single element' => :pass, 'with no elements' => :pass, 'with multiple elements' => :fail }, + '0..*' => { 'with a single element' => :pass, 'with no elements' => :pass, 'with multiple elements' => :pass }, + '1..1' => { 'with a single element' => :pass, 'with no elements' => :fail, 'with multiple elements' => :fail }, + '1..*' => { 'with a single element' => :pass, 'with no elements' => :fail, 'with multiple elements' => :pass } } scenarios.each do |cardinality, number_of_elements_scenario| - context "when the cardinality is #{cardinality[0]}..#{cardinality[-1]}" do - let(:element_definition) do FHIR::ElementDefinition.new(min: cardinality[0], - max: cardinality[-1], - id: 'Patient.id', - path: 'Patient.id') - end - number_of_elements_scenario.each do |number_of_elements, expected_result| - context "when the element contains #{number_of_elements}" do - include_context number_of_elements - it "#{expected_result} when the element contains #{number_of_elements}" do - resource.id = id - results = validator.validate(resource, element_definition) - expect(results.size).to eq(1) - expect(results).to all(have_attributes(validation_type: :cardinality)) - expect(results).to all(have_attributes(result: expected_result)) + number_of_elements_scenario.each do |number_of_elements, expected_result| + context "when the cardinality is #{cardinality[0]}..#{cardinality[-1]} and the element contains #{number_of_elements}" do + let(:element_definition) do + FHIR::ElementDefinition.new(min: cardinality[0], + max: cardinality[-1], + id: 'Patient.id', + path: 'Patient.id') + end + + let(:id) do + case number_of_elements + when 'with a single element' + 'the id' + when 'with no elements' + nil + when 'with multiple elements' + [1, 2] end end + + let(:results) do + resource.id = id + validator.validate(resource, element_definition) + end + + it 'includes one result' do + expect(results.size).to eq(1) + end + + it 'all results have a validation_type of cardinality' do + expect(results).to all(have_attributes(validation_type: :cardinality)) + end + + it "the result status is #{expected_result}" do + expect(results).to all(have_attributes(result: expected_result)) + end end end end end -end \ No newline at end of file +end From c4cb6b2c150c3361af943c16936b77b46164089f Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Tue, 13 Aug 2019 18:03:38 -0400 Subject: [PATCH 69/80] clean up data type validator test --- .../data_type_validator_spec.rb | 95 ++++++++++++++----- 1 file changed, 69 insertions(+), 26 deletions(-) diff --git a/spec/unit/validation/element_validator/data_type_validator_spec.rb b/spec/unit/validation/element_validator/data_type_validator_spec.rb index d28324aa1..947c8b738 100644 --- a/spec/unit/validation/element_validator/data_type_validator_spec.rb +++ b/spec/unit/validation/element_validator/data_type_validator_spec.rb @@ -1,71 +1,114 @@ describe FHIR::Validation::DataTypeValidator do - - let(:validator) { FHIR::Validation::DataTypeValidator } + let(:validator) { described_class } let(:resource) do FHIR::Patient.new(id: 2, - name: {given: 'Bob'}, - communication: [{language: 'English'}, {language: 'Spanish'}], + name: { given: 'Bob' }, + communication: [{ language: 'English' }, { language: 'Spanish' }], deceasedBoolean: false, deceasedDateTime: 'YYYY-MM-DD') end + shared_context 'with results' do + let(:results) { validator.validate(resource, element_definition) } + end + + shared_examples 'data type result' do + it 'returns at least one result' do + expect(results).not_to be_empty + end + + it 'returns no failing results' do + expect(results.select { |res| res.result == :fail }).to be_empty + end + end + + shared_examples 'cardinality results' do + it 'returns cardinality results' do + expect(results.select { |res| res.validation_type == :cardinality }).not_to be_empty + end + end + describe '#validate' do let(:element_definition) do FHIR::ElementDefinition.new(id: 'Patient', path: 'Patient', - type: {code: 'Patient'}) + type: { code: 'Patient' }) end + + include_context 'results' + it 'skips the root element' do results = validator.validate(resource, element_definition) expect(results).to be_nil end - context 'with an element with a single type' do + + context 'with a valid element with a single type' do let(:element_definition) do FHIR::ElementDefinition.new(id: 'Patient.name', path: 'Patient.name', - type: {code: 'HumanName'}) + type: { code: 'HumanName' }) end - it 'returns an array of results for the single type' do - results = validator.validate(resource, element_definition) - expect(results).to_not be_empty - expect(results.select {|res| res.result == :fail}).to be_empty - expect(results.select {|res| res.validation_type == :cardinality}).to_not be_empty + + include_examples 'data type result' + include_examples 'cardinality results' + end + + context 'when the type of the element is unknown' do + let(:element_definition) do + FHIR::ElementDefinition.new(id: 'Patient.name', + path: 'Patient.name', + type: { code: 'Foo' }) end - it 'Returns a warning if the type is unknown' do - element_definition.type.first.code = 'Foo' - results = validator.validate(resource, element_definition) + + include_context 'results' + + include_examples 'data type result' + + it 'returns results with warnings' do expect(results).to all(have_attributes(result: :warn)) - expect(results.first).to have_attributes(text: "Unknown type: Foo") + end + + it 'lists the unknown type' do + expect(results.first).to have_attributes(text: 'Unknown type: Foo') end end + context 'with an element that has a choice of types' do let(:resource) do - obs = FHIR::Observation.new(valueQuantity: {value: 2, code: 'mm'}) - obs.valueCodeableConcept = FHIR::CodeableConcept.new(coding: {code: 'foo', system: 'bar'}, text: 'fake') + obs = FHIR::Observation.new(valueQuantity: { value: 2, code: 'mm' }) + obs.valueCodeableConcept = FHIR::CodeableConcept.new(coding: { code: 'foo', system: 'bar' }, text: 'fake') obs end + let(:element_definition) do FHIR::ElementDefinition.new(id: 'Observation.value[x]', path: 'Observation.value[x]', type: types) end - let(:type1) { {code: 'Quantity'} } + let(:results) { validator.validate(resource, element_definition) } - let(:type2) { {code: 'CodeableConcept'} } + let(:type1) { { code: 'Quantity' } } + + let(:type2) { { code: 'CodeableConcept' } } let(:types) do [type1, type2] end - it 'returns an array of results for elements with a choice of type' do - results = validator.validate(resource, element_definition) - expect(results).to_not be_empty + + include_context 'results' + + include_examples 'data type result' + include_examples 'cardinality results' + + it 'includes results for the first type' do expect(results).to include(have_attributes(element_path: a_string_including(element_definition.type[0].code))) + end + + it 'includes results for the second type' do expect(results).to include(have_attributes(element_path: a_string_including(element_definition.type[1].code))) - expect(results.select {|res| res.result == :fail}).to be_empty - expect(results.select {|res| res.validation_type == :cardinality}).to_not be_empty end end end -end \ No newline at end of file +end From 10980ddff73d75582d25f4a336daa10c8299fe46 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Tue, 13 Aug 2019 18:29:56 -0400 Subject: [PATCH 70/80] cleanup fixed_value_validator spec --- .../fixed_value_validator_spec.rb | 60 ++++++++++++------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/spec/unit/validation/element_validator/fixed_value_validator_spec.rb b/spec/unit/validation/element_validator/fixed_value_validator_spec.rb index e1af249af..7c0ba5d72 100644 --- a/spec/unit/validation/element_validator/fixed_value_validator_spec.rb +++ b/spec/unit/validation/element_validator/fixed_value_validator_spec.rb @@ -1,35 +1,51 @@ describe FHIR::Validation::FixedValueValidator do + let(:validator) { described_class } - let(:validator) { FHIR::Validation::FixedValueValidator } - - let(:fixed_value) {'867-5309'} + let(:fixed_value) { '867-5309' } let(:element) { FHIR::Patient.new(gender: fixed_value) } - #let(:element_definition) { instance_double(FHIR::ElementDefinition) } - let(:element_definition) do FHIR::ElementDefinition.new(id: 'Patient.gender', - path: 'Patient.gender', - type:[{code: 'String'}]) - end - describe '#validate' do - it 'returns a single result related to the fixed value' do + let(:id_and_path) { 'Patient.gender' } - # allow(element_definition).to receive(:fixed) - # .and_return(element) - element_definition.fixedString = fixed_value + let(:element_definition) do + FHIR::ElementDefinition.new(id: 'Patient.gender', + path: 'Patient.gender', + type: [{ code: 'String' }]) + end - results = validator.validate(element, element_definition) - expect(results.first.validation_type).to be(:fixed_value) - expect(results.first.result).to be(:pass) + shared_examples 'fixed value results' do + expect(results).to all(have_attributes(validation_type: :fixed_value)) + end + + describe '#validate' do + context 'when the fixed string matches' do + let(:element_definition) do + FHIR::ElementDefinition.new(id: id_and_path, + path: id_and_path, + type: [{ code: 'String' }], + fixedString: fixed_value) + end + + let(:results) { validator.validate(element, element_definition) } + + it 'the validation passes' do + expect(results).to all(have_attributes(result: :pass)) + end end - it 'detects when the fixed value is incorrect' do + context 'when the fixed string does not match' do + let(:element_definition) do + FHIR::ElementDefinition.new(id: id_and_path, + path: id_and_path, + type: [{ code: 'String' }], + fixedString: 'INVALID_FIXED') + end - element_definition.fixedString = 'INVALID_FIXED' + let(:results) { validator.validate(element, element_definition) } - results = validator.validate(element, element_definition) - expect(results.first.validation_type).to be(:fixed_value) - expect(results.first.result).to be(:fail) + it 'the validation fails' do + expect(results).to all(have_attributes(result: :fail)) + end end end -end \ No newline at end of file +end From 3bb46efd329f1efefaf0fffa53936a6cf90ed278 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Tue, 13 Aug 2019 18:39:44 -0400 Subject: [PATCH 71/80] cleanup max length validator spec --- .../max_length_validator_spec.rb | 56 ++++++++++--------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/spec/unit/validation/element_validator/max_length_validator_spec.rb b/spec/unit/validation/element_validator/max_length_validator_spec.rb index 1a334c3d6..2a8d46b5c 100644 --- a/spec/unit/validation/element_validator/max_length_validator_spec.rb +++ b/spec/unit/validation/element_validator/max_length_validator_spec.rb @@ -1,49 +1,55 @@ describe FHIR::Validation::MaxLengthValidator do - let(:validator) {FHIR::Validation::MaxLengthValidator} - let(:restricted_string) {'foo'} + let(:validator) { described_class } + let(:restricted_string) { 'foo' } let(:resource) { FHIR::Patient.new(gender: restricted_string) } - let(:element_definition) {FHIR::ElementDefinition.new(id: 'Patient.gender', path: 'Patient.gender')} + let(:element_definition) { FHIR::ElementDefinition.new(id: 'Patient.gender', path: 'Patient.gender') } - context 'when the string is shorter than the maxLength' do - before do - element_definition.maxLength = 10 - @results = validator.validate(resource, element_definition) + shared_examples 'max length results' do + it 'results have validation type :max_length' do + expect(results).to all(have_attributes(validation_type: :max_length)) end + end + shared_examples 'returns one result' do it 'returns one result' do - expect(@results.size).to eq(1) + expect(results.size).to eq(1) end + end + + shared_context 'with with results for max length' do |max_length| + let(:results) do + element_definition.maxLength = max_length + validator.validate(resource, element_definition) + end + end + + context 'when the string is shorter than the maxLength' do + include_context 'with with results for max length', 10 + + include_examples 'returns one result' + include_examples 'max length results' it 'passes' do - expect(@results.first).to have_attributes(validation_type: :max_length) - expect(@results.first).to have_attributes(result: :pass) + expect(results).to all(have_attributes(result: :pass)) end end context 'when the string is longer than the maxLength' do - before do - element_definition.maxLength = 2 - @results = validator.validate(resource, element_definition) - end + include_context 'with with results for max length', 2 - it 'returns one result' do - expect(@results.size).to eq(1) - end + include_examples 'returns one result' + include_examples 'max length results' it 'fails' do - expect(@results.first).to have_attributes(validation_type: :max_length) - expect(@results.first).to have_attributes(result: :fail) + expect(results).to all(have_attributes(result: :fail)) end end context 'when no max length is specified' do - before do - element_definition.maxLength = nil - @results = validator.validate(resource, element_definition) - end + include_context 'with with results for max length', nil it 'returns no results' do - expect(@results).to be_nil + expect(results).to be_nil end end -end \ No newline at end of file +end From 884511b9f74307d0cbfad9c9d19b4800b1ff3ac8 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Tue, 13 Aug 2019 18:43:04 -0400 Subject: [PATCH 72/80] more spec cleanup --- .rubocop.yml | 5 +- spec/lib/fhir_models/bootstrap/model_spec.rb | 19 +++-- spec/unit/bootstrap/definitions_spec.rb | 2 +- spec/unit/bootstrap/model_spec.rb | 45 ++++++------ spec/unit/fhir_ext/element_definition_spec.rb | 25 ++++--- .../terminology_validator_spec.rb | 73 +++++++++---------- spec/unit/validation/retrieval_spec.rb | 63 ++++++++-------- .../validation/structure_validator_spec.rb | 23 +++--- 8 files changed, 129 insertions(+), 126 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index ae408fa08..a8788182f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -20,4 +20,7 @@ inherit_mode: Naming/RescuedExceptionsVariableName: PreferredName: 'e' RSpec/FilePath: - Enabled: false \ No newline at end of file + Enabled: false +Metrics/BlockLength: + Exclude: + - 'spec/**/*.rb' \ No newline at end of file diff --git a/spec/lib/fhir_models/bootstrap/model_spec.rb b/spec/lib/fhir_models/bootstrap/model_spec.rb index 1df2ecf3b..cae25f7ad 100644 --- a/spec/lib/fhir_models/bootstrap/model_spec.rb +++ b/spec/lib/fhir_models/bootstrap/model_spec.rb @@ -3,7 +3,7 @@ it 'should be the same for two identical fhir models' do attributes = { name: [ - family: [ 'Smith' ] + family: ['Smith'] ] } patient1 = FHIR::Patient.new(attributes) @@ -14,12 +14,12 @@ it 'should be different for two models that do not have the same attributes' do attributes1 = { name: [ - family: [ 'Smith' ] + family: ['Smith'] ] } attributes2 = { name: [ - family: [ 'Jones' ] + family: ['Jones'] ] } patient1 = FHIR::Patient.new(attributes1) @@ -32,7 +32,7 @@ it 'should be true for two identical fhir models' do attributes = { name: [ - family: [ 'Smith' ] + family: ['Smith'] ] } patient1 = FHIR::Patient.new(attributes) @@ -44,12 +44,12 @@ it 'should be false for two models that do not have the same attributes' do attributes1 = { name: [ - family: [ 'Smith' ] + family: ['Smith'] ] } attributes2 = { name: [ - family: [ 'Jones' ] + family: ['Jones'] ] } patient1 = FHIR::Patient.new(attributes1) @@ -61,11 +61,11 @@ it 'should be false when compared to a different class' do attributes1 = { name: [ - family: [ 'Smith' ] + family: ['Smith'] ] } patient1 = FHIR::Patient.new(attributes1) - patient2 = "patient 2" + patient2 = 'patient 2' expect(patient1).not_to eq patient2 expect(patient1).not_to eql patient2 end @@ -73,13 +73,12 @@ it 'should be false when compared to nil' do attributes1 = { name: [ - family: [ 'Smith' ] + family: ['Smith'] ] } patient1 = FHIR::Patient.new(attributes1) expect(patient1).not_to eq nil expect(patient1).not_to be_nil end - end end diff --git a/spec/unit/bootstrap/definitions_spec.rb b/spec/unit/bootstrap/definitions_spec.rb index b22fbefd0..516207d34 100644 --- a/spec/unit/bootstrap/definitions_spec.rb +++ b/spec/unit/bootstrap/definitions_spec.rb @@ -1,7 +1,7 @@ describe FHIR::Definitions do context '.load_extensions' - context '.get_display' do + context 'get_display' do let(:code) { '306005' } let(:uri) { 'http://snomed.info/sct' } diff --git a/spec/unit/bootstrap/model_spec.rb b/spec/unit/bootstrap/model_spec.rb index ac5726848..04320dfe8 100644 --- a/spec/unit/bootstrap/model_spec.rb +++ b/spec/unit/bootstrap/model_spec.rb @@ -2,45 +2,46 @@ describe '#retrieve_elements_by_definition' do let(:resource) do FHIR::Patient.new(id: 2, - extension: [{url: 'bar'}, {url: 'http://foo.org'}], - name: {given: 'Bob'}, - communication: [{language: 'English'}, {language: 'Spanish'}], + extension: [{ url: 'bar' }, { url: 'http://foo.org' }], + name: { given: 'Bob' }, + communication: [{ language: 'English' }, { language: 'Spanish' }], deceasedBoolean: false, deceasedDateTime: 'YYYY-MM-DD') end it 'returns the root element (the resource)' do element_definition = FHIR::ElementDefinition.new(id: 'Patient', path: 'Patient') - expect(resource.retrieve_elements_by_definition(element_definition)).to eq({element_definition.id => resource}) + expect(resource.retrieve_elements_by_definition(element_definition)).to eq(element_definition.id => resource) end it 'returns an attribute of element' do element_definition = FHIR::ElementDefinition.new(id: 'Patient.id', path: 'Patient.id') - expect(resource.retrieve_elements_by_definition(element_definition)).to eq({element_definition.id => resource.id}) + expect(resource.retrieve_elements_by_definition(element_definition)).to eq(element_definition.id => resource.id) end context 'with choice of types' do let(:element_definition) do FHIR::ElementDefinition.new(id: 'Patient.deceased[x]', path: 'Patient.deceased[x]', - type: [{code: 'boolean'}, {code: 'dateTime'}]) + type: [{ code: 'boolean' }, { code: 'dateTime' }]) end it 'returns all the elements present if there is a choice of type' do titlecase_choice = [ - element_definition.id.gsub('[x]', - element_definition.type.first.code.capitalize), - element_definition.id.gsub('[x]', - "#{element_definition.type.last.code[0].capitalize}"\ - "#{element_definition.type.last.code[1..-1]}")] + element_definition.id.gsub('[x]', + element_definition.type.first.code.capitalize), + element_definition.id.gsub('[x]', + "#{element_definition.type.last.code[0].capitalize}"\ + "#{element_definition.type.last.code[1..-1]}") + ] expected_communications = { - titlecase_choice.first => resource.deceasedBoolean, - titlecase_choice.last => resource.deceasedDateTime + titlecase_choice.first => resource.deceasedBoolean, + titlecase_choice.last => resource.deceasedDateTime } expect(resource.retrieve_elements_by_definition(element_definition)).to eq(expected_communications) end it 'returns an array of all the elements present if there is a choice of type normalized to the element' do - expected_communications = { element_definition.id => [ resource.deceasedBoolean, resource.deceasedDateTime ]} + expected_communications = { element_definition.id => [resource.deceasedBoolean, resource.deceasedDateTime] } expect(resource.retrieve_elements_by_definition(element_definition, normalized: true)).to eq(expected_communications) end end @@ -48,11 +49,11 @@ context 'with sliced extensions' do it 'returns all extensions' do element_definition = FHIR::ElementDefinition.new(id: 'Patient.extension', path: 'Patient.extension') - expect(resource.retrieve_elements_by_definition(element_definition)).to eq({element_definition.id => resource.extension}) + expect(resource.retrieve_elements_by_definition(element_definition)).to eq(element_definition.id => resource.extension) end it 'returns the extensions indexed' do - expected_result = {'Patient.extension[0]' => resource.extension[0], - 'Patient.extension[1]' => resource.extension[1]} + expected_result = { 'Patient.extension[0]' => resource.extension[0], + 'Patient.extension[1]' => resource.extension[1] } element_definition = FHIR::ElementDefinition.new(id: 'Patient.extension', path: 'Patient.extension') expect(resource.retrieve_elements_by_definition(element_definition, indexed: true)).to eq(expected_result) end @@ -60,17 +61,17 @@ element_definition = FHIR::ElementDefinition.new(id: 'Patient.extension:foo', path: 'Patient.extension', sliceName: 'foo', - type: [{code: 'Extension', profile: ['http://foo.org']}]) - expect(resource.retrieve_elements_by_definition(element_definition)).to eq({element_definition.path => [resource.extension[1]]}) + type: [{ code: 'Extension', profile: ['http://foo.org'] }]) + expect(resource.retrieve_elements_by_definition(element_definition)).to eq(element_definition.path => [resource.extension[1]]) end it 'returns the sliced extension indexed' do element_definition = FHIR::ElementDefinition.new(id: 'Patient.extension:foo', path: 'Patient.extension', sliceName: 'foo', - type: [{code: 'Extension', profile: ['http://foo.org']}]) - expect(resource.retrieve_elements_by_definition(element_definition, indexed: true)).to eq({"#{element_definition.path}[1]" => resource.extension[1]}) + type: [{ code: 'Extension', profile: ['http://foo.org'] }]) + expect(resource.retrieve_elements_by_definition(element_definition, indexed: true)).to eq("#{element_definition.path}[1]" => resource.extension[1]) end end end -end \ No newline at end of file +end diff --git a/spec/unit/fhir_ext/element_definition_spec.rb b/spec/unit/fhir_ext/element_definition_spec.rb index e78b17249..c68a448d3 100644 --- a/spec/unit/fhir_ext/element_definition_spec.rb +++ b/spec/unit/fhir_ext/element_definition_spec.rb @@ -1,14 +1,13 @@ describe FHIR::ElementDefinition do - - shared_context 'choice of type' do + shared_context 'with a choice of type' do let(:element_definition) do - FHIR::ElementDefinition.new(path: 'Patient.deceased[x]', type: [{code: 'boolean'}, {code: 'dateTime'}]) + described_class.new(path: 'Patient.deceased[x]', type: [{ code: 'boolean' }, { code: 'dateTime' }]) end end - shared_context 'single type' do + shared_context 'with a single type' do let(:element_definition) do - FHIR::ElementDefinition.new(path: 'Patient.deceased', type: [{code: 'boolean'}]) + described_class.new(path: 'Patient.deceased', type: [{ code: 'boolean' }]) end end describe '#choice_type' do @@ -30,15 +29,22 @@ describe '#type_code' do context 'with a single type' do include_context 'single type' - it 'provides the expected type of the element' do - expect(element_definition.type_code).to eq('boolean') + it 'provides the expected type of the element when provided a path' do expect(element_definition.type_code('fake.path')).to eq('boolean') end + + it 'provides the expected type of the element without a path provided' do + expect(element_definition.type_code).to eq('boolean') + end end + context 'with a choice of types' do include_context 'choice of type' - it 'provides the correct type from the choices' do + it 'provides the correct type from the choices when the first type is provided' do expect(element_definition.type_code('Patient.deceasedBoolean')).to eq('boolean') + end + + it 'provides the correct type from the choices when the second type is provided' do expect(element_definition.type_code('Patient.deceasedDateTime')).to eq('dateTime') end @@ -60,6 +66,7 @@ expect(element_definition.type_paths).to eq(['Patient.deceased']) end end + context 'with a choice of types' do include_context 'choice of type' it 'returns a path for each type' do @@ -67,4 +74,4 @@ end end end -end \ No newline at end of file +end diff --git a/spec/unit/validation/element_validator/terminology_validator_spec.rb b/spec/unit/validation/element_validator/terminology_validator_spec.rb index eeec9ae35..d8f703539 100644 --- a/spec/unit/validation/element_validator/terminology_validator_spec.rb +++ b/spec/unit/validation/element_validator/terminology_validator_spec.rb @@ -1,36 +1,35 @@ describe FHIR::Validation::TerminologyValidator do - let(:in_system) do lambda do true end end - data_types = ['CodeableConcept', 'Quantity', 'Coding'] - bindings = ['required', 'extensible', 'preferred', 'example'] + data_types = %w[CodeableConcept Quantity Coding] + bindings = %w[required extensible preferred example] data_binding_pairs = data_types.product(bindings) validation_options = [true, false] - validation_combinations = validation_options.product(*Array.new(3,validation_options)) + validation_combinations = Array.new(3, validation_options) shared_context 'ValueSet is known' do |known| - known ? let(:value_set) {'foo'} : let(:value_set) {'bar'} + known ? let(:value_set) { 'foo' } : let(:value_set) { 'bar' } end shared_context 'CodeSystem is known' do |known| - known ? let(:code_system) {'qux'} : let(:code_system) {'corge'} + known ? let(:code_system) { 'qux' } : let(:code_system) { 'corge' } end shared_context 'code is in the ValueSet' do |included| let(:value_set_validator) do - {'foo' => lambda { |coding| included}} + { 'foo' => ->(_coding) { included } } end end shared_context 'code is in the CodeSystem' do |included| let(:code_system_validator) do - {'qux' => lambda { |coding| included}} + { 'qux' => ->(_coding) { included } } end end @@ -38,7 +37,7 @@ let(:resource) do case resource when 'CodeableConcept' - FHIR::CodeableConcept.new(coding: {code: 'waldo', system: code_system}) + FHIR::CodeableConcept.new(coding: { code: 'waldo', system: code_system }) when 'Quantity' FHIR::Quantity.new(code: 'waldo', system: code_system) when 'Coding' @@ -49,16 +48,16 @@ shared_context 'ElementDefinition' do |binding_strength, type, second_type = nil| let(:element_definition) do - types = [{code: type}] - types.push({code: second_type}) if second_type + types = [{ code: type }] + types.push(code: second_type) if second_type FHIR::ElementDefinition.new(id: "Element#{'[x]' if second_type}", path: "Element#{'[x]' if second_type}", - binding: {strength: binding_strength, valueSet: value_set}, + binding: { strength: binding_strength, valueSet: value_set }, type: types) end end - let(:validator) { FHIR::Validation::TerminologyValidator.new(value_set_validator.merge(code_system_validator))} + let(:validator) { FHIR::Validation::TerminologyValidator.new(value_set_validator.merge(code_system_validator)) } shared_examples 'expected results' do |validation_combo, binding_strength| required_strength = (binding_strength == 'required') @@ -71,13 +70,13 @@ if validation_combo[0] it 'checks for inclusion in the ValueSet' do expect(@results.first).to have_attributes(validation_type: :terminology) - expect(@results.first).to have_attributes(:text => a_string_including("codings from #{value_set}")) + expect(@results.first).to have_attributes(text: a_string_including("codings from #{value_set}")) expect(@results.first).to have_attributes(result: (validation_combo[2] ? :pass : fail_strength)) end else it 'warns that the ValueSet validator is missing' do expect(@results.first).to have_attributes(validation_type: :terminology) - expect(@results.first).to have_attributes(:text => a_string_including('Missing Validator for bar')) + expect(@results.first).to have_attributes(text: a_string_including('Missing Validator for bar')) expect(@results.first).to have_attributes(result: :warn) end end @@ -85,13 +84,13 @@ if validation_combo[1] it 'checks for inclusion in the CodeSystem' do expect(@results.last).to have_attributes(validation_type: :terminology) - expect(@results.last).to have_attributes(:text => a_string_including("codings from #{code_system}")) + expect(@results.last).to have_attributes(text: a_string_including("codings from #{code_system}")) expect(@results.last).to have_attributes(result: (validation_combo[3] ? :pass : :fail)) end else it 'warns that the CodeSystem validator is missing' do expect(@results.last).to have_attributes(validation_type: :terminology) - expect(@results.last).to have_attributes(:text => a_string_including('Missing Validator for corge')) + expect(@results.last).to have_attributes(text: a_string_including('Missing Validator for corge')) expect(@results.last).to have_attributes(result: :warn) end end @@ -99,7 +98,7 @@ context 'with no code or valueset validators' do include_context 'code is in the ValueSet' - let(:validator) {FHIR::Validation::TerminologyValidator.new} + let(:validator) { FHIR::Validation::TerminologyValidator.new } it 'contains no validators initially' do expect(validator.vs_validators).to be_empty end @@ -113,44 +112,42 @@ end it 'warns if the code is missing' do - results = validator.validate(FHIR::CodeableConcept.new(coding: {system: 'adsf'}), - FHIR::ElementDefinition.new(id: 'Element', - path: 'Element', - binding: {strength: 'required', valueSet: 'asdf'}, - type: [{code: 'CodeableConcept'}])) + results = validator.validate(FHIR::CodeableConcept.new(coding: { system: 'adsf' }), + FHIR::ElementDefinition.new(id: 'Element', + path: 'Element', + binding: { strength: 'required', valueSet: 'asdf' }, + type: [{ code: 'CodeableConcept' }])) expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :terminology)) - expect(results).to all(have_attributes(:text => a_string_including('missing code'))) + expect(results).to all(have_attributes(text: a_string_including('missing code'))) expect(results).to all(have_attributes(result: :warn)) - end it 'warns if the system is missing' do - results = validator.validate(FHIR::CodeableConcept.new(coding: {code: 'waldo'}), - FHIR::ElementDefinition.new(id: 'Element', - path: 'Element', - binding: {strength: 'required', valueSet: 'asdf'}, - type: [{code: 'CodeableConcept'}])) + results = validator.validate(FHIR::CodeableConcept.new(coding: { code: 'waldo' }), + FHIR::ElementDefinition.new(id: 'Element', + path: 'Element', + binding: { strength: 'required', valueSet: 'asdf' }, + type: [{ code: 'CodeableConcept' }])) expect(results.size).to eq(1) expect(results).to all(have_attributes(validation_type: :terminology)) - expect(results).to all(have_attributes(:text => a_string_including('missing system'))) + expect(results).to all(have_attributes(text: a_string_including('missing system'))) expect(results).to all(have_attributes(result: :warn)) end it 'warns if the code and system are missing' do - results = validator.validate(FHIR::CodeableConcept.new(coding: {text: 'nope'}), + results = validator.validate(FHIR::CodeableConcept.new(coding: { text: 'nope' }), FHIR::ElementDefinition.new(id: 'Element', path: 'Element', - binding: {strength: 'required', valueSet: 'asdf'}, - type: [{code: 'CodeableConcept'}])) + binding: { strength: 'required', valueSet: 'asdf' }, + type: [{ code: 'CodeableConcept' }])) expect(results.size).to eq(2) expect(results).to all(have_attributes(validation_type: :terminology)) - expect(results).to all(have_attributes(:text => a_string_including('missing'))) + expect(results).to all(have_attributes(text: a_string_including('missing'))) expect(results).to all(have_attributes(result: :warn)) end - end describe '#validate' do @@ -161,7 +158,7 @@ include_context 'Resource type', data_binding_pair[0] validation_combinations.each do |combo| context "#{'un' unless combo[0]}known ValueSet #{'not ' unless combo[2]}containing the code, and "\ - "#{'un' unless combo[1]}known CodeSystem #{'not ' unless combo[3]}containing the code"do + "#{'un' unless combo[1]}known CodeSystem #{'not ' unless combo[3]}containing the code" do include_context 'ValueSet is known', combo[0] include_context 'CodeSystem is known', combo[1] include_context 'code is in the ValueSet', combo[2] @@ -176,4 +173,4 @@ end end end -end \ No newline at end of file +end diff --git a/spec/unit/validation/retrieval_spec.rb b/spec/unit/validation/retrieval_spec.rb index 48d18a2a3..81b2e6d06 100644 --- a/spec/unit/validation/retrieval_spec.rb +++ b/spec/unit/validation/retrieval_spec.rb @@ -1,10 +1,10 @@ describe FHIR::Validation::Retrieval do - let(:retrieval) { FHIR::Validation::Retrieval } + let(:retrieval) { described_class } let(:resource) do FHIR::Patient.new(id: 2, - extension: [{url: 'bar'}, {url: 'http://foo.org'}], - name: {given: 'Bob'}, - communication: [{language: 'English'}, {language: 'Spanish'}], + extension: [{ url: 'bar' }, { url: 'http://foo.org' }], + name: { given: 'Bob' }, + communication: [{ language: 'English' }, { language: 'Spanish' }], deceasedBoolean: false, deceasedDateTime: 'YYYY-MM-DD') end @@ -12,24 +12,25 @@ describe '#retrieve_by_element_definition' do it 'returns the root element of a resource' do element_definition = FHIR::ElementDefinition.new(id: 'Patient', path: 'Patient') - expect(retrieval.retrieve_by_element_definition(resource, element_definition)).to eq({element_definition.id => resource}) + expect(retrieval.retrieve_by_element_definition(resource, element_definition)).to eq(element_definition.id => resource) end it 'returns an attribute of element' do element_definition = FHIR::ElementDefinition.new(id: 'Patient.id', path: 'Patient.id') - expect(retrieval.retrieve_by_element_definition(resource, element_definition)).to eq({element_definition.id => resource.id}) + expect(retrieval.retrieve_by_element_definition(resource, element_definition)).to eq(element_definition.id => resource.id) end context 'with elements that have a cardinality greater than one' do - let(:element_definition) {FHIR::ElementDefinition.new(id: 'Patient.communication', path: 'Patient.communication')} + let(:element_definition) { FHIR::ElementDefinition.new(id: 'Patient.communication', path: 'Patient.communication') } + it 'returns attributes as an array' do - expect(retrieval.retrieve_by_element_definition(resource, element_definition)).to eq({element_definition.id => resource.communication}) + expect(retrieval.retrieve_by_element_definition(resource, element_definition)).to eq(element_definition.id => resource.communication) end it 'returns elements indexed' do expected_communications = { - "#{element_definition.id}[0]" => resource.communication.first, - "#{element_definition.id}[1]" => resource.communication.last + "#{element_definition.id}[0]" => resource.communication.first, + "#{element_definition.id}[1]" => resource.communication.last } expect(retrieval.retrieve_by_element_definition(resource, element_definition, indexed: true)).to eq(expected_communications) end @@ -39,24 +40,18 @@ let(:element_definition) do FHIR::ElementDefinition.new(id: 'Patient.deceased[x]', path: 'Patient.deceased[x]', - type: [{code: 'boolean'}, {code: 'dateTime'}]) + type: [{ code: 'boolean' }, { code: 'dateTime' }]) end + it 'returns all the elements present if there is a choice of type' do - titlecase_choice = [ - element_definition.id.gsub('[x]', - element_definition.type.first.code.capitalize), - element_definition.id.gsub('[x]', - "#{element_definition.type.last.code[0].capitalize}"\ - "#{element_definition.type.last.code[1..-1]}")] - expected_communications = { - titlecase_choice.first => resource.deceasedBoolean, - titlecase_choice.last => resource.deceasedDateTime - } + titlecase_choice = [element_definition.id.gsub('[x]', element_definition.type.first.code.capitalize), + element_definition.id.gsub('[x]', "#{element_definition.type.last.code[0].capitalize}#{element_definition.type.last.code[1..-1]}")] + expected_communications = { titlecase_choice.first => resource.deceasedBoolean, titlecase_choice.last => resource.deceasedDateTime } expect(retrieval.retrieve_by_element_definition(resource, element_definition)).to eq(expected_communications) end it 'returns an array of all the elements present if there is a choice of type normalized to the element' do - expected_communications = { element_definition.id => [ resource.deceasedBoolean, resource.deceasedDateTime ]} + expected_communications = { element_definition.id => [resource.deceasedBoolean, resource.deceasedDateTime] } expect(retrieval.retrieve_by_element_definition(resource, element_definition, normalized: true)).to eq(expected_communications) end end @@ -64,11 +59,11 @@ context 'with sliced extensions' do it 'returns all extensions' do element_definition = FHIR::ElementDefinition.new(id: 'Patient.extension', path: 'Patient.extension') - expect(retrieval.retrieve_by_element_definition(resource, element_definition)).to eq({element_definition.id => resource.extension}) + expect(retrieval.retrieve_by_element_definition(resource, element_definition)).to eq(element_definition.id => resource.extension) end it 'returns the extensions indexed' do - expected_result = {'Patient.extension[0]' => resource.extension[0], - 'Patient.extension[1]' => resource.extension[1]} + expected_result = { 'Patient.extension[0]' => resource.extension[0], + 'Patient.extension[1]' => resource.extension[1] } element_definition = FHIR::ElementDefinition.new(id: 'Patient.extension', path: 'Patient.extension') expect(retrieval.retrieve_by_element_definition(resource, element_definition, indexed: true)).to eq(expected_result) end @@ -76,27 +71,29 @@ element_definition = FHIR::ElementDefinition.new(id: 'Patient.extension:foo', path: 'Patient.extension', sliceName: 'foo', - type: [{code: 'Extension', profile: ['http://foo.org']}]) - expect(retrieval.retrieve_by_element_definition(resource, element_definition)).to eq({element_definition.path => [resource.extension[1]]}) + type: [{ code: 'Extension', profile: ['http://foo.org'] }]) + expect(retrieval.retrieve_by_element_definition(resource, element_definition)).to eq(element_definition.path => [resource.extension[1]]) end it 'returns the sliced extension indexed' do element_definition = FHIR::ElementDefinition.new(id: 'Patient.extension:foo', path: 'Patient.extension', sliceName: 'foo', - type: [{code: 'Extension', profile: ['http://foo.org']}]) - expect(retrieval.retrieve_by_element_definition(resource, element_definition, indexed: true)).to eq({"#{element_definition.path}[1]" => resource.extension[1]}) + type: [{ code: 'Extension', profile: ['http://foo.org'] }]) + expect(retrieval.retrieve_by_element_definition(resource, element_definition, indexed: true)).to eq("#{element_definition.path}[1]" => resource.extension[1]) end end end describe '#retrieve_element_with_fhirpath' do - let(:element_definition) {FHIR::ElementDefinition.new(id: id, path: path)} + let(:element_definition) { FHIR::ElementDefinition.new(id: id, path: path) } + context 'when retrieving the root element' do - let(:id) {'Patient'} - let(:path) {'Patient'} + let(:id) { 'Patient' } + let(:path) { 'Patient' } + it 'returns the root element (the resource)' do - expect(retrieval.retrieve_by_element_definition(resource, element_definition)).to eq({element_definition.id => resource}) + expect(retrieval.retrieve_by_element_definition(resource, element_definition)).to eq(element_definition.id => resource) end end end diff --git a/spec/unit/validation/structure_validator_spec.rb b/spec/unit/validation/structure_validator_spec.rb index 3e7964fe3..ae96e2ff4 100644 --- a/spec/unit/validation/structure_validator_spec.rb +++ b/spec/unit/validation/structure_validator_spec.rb @@ -6,29 +6,28 @@ # FHIR::Validation::StructureValidator.new(profile) # end # - let(:profile) {FHIR::StructureDefinition.new(snapshot: {element: elements})} + let(:profile) { FHIR::StructureDefinition.new(snapshot: { element: elements }) } let(:elements) do [FHIR::ElementDefinition.new(id: 'Patient', - path: 'Patient', - min: 0, - max: '*'), + path: 'Patient', + min: 0, + max: '*'), FHIR::ElementDefinition.new(id: 'Patient.extension', - path: 'Patient.extension', - min: 0, - max: '*'), + path: 'Patient.extension', + min: 0, + max: '*'), FHIR::ElementDefinition.new(id: 'Patient.extension:foo', path: 'Patient.extension', sliceName: 'foo', min: 0, max: '*', type: [ - {code: 'Extension', profile: ['http://foo.org']} - ]), - ] + { code: 'Extension', profile: ['http://foo.org'] } + ])] end let(:resource) do - FHIR::Patient.new(extension: [{url: 'bar'}, {url: 'http://foo.org'}]) + FHIR::Patient.new(extension: [{ url: 'bar' }, { url: 'http://foo.org' }]) end let(:validator) do @@ -44,4 +43,4 @@ expect(cardinality_results.length.positive?).to be_truthy end end -end \ No newline at end of file +end From da597eb92c6ad2dae595394245dea2649f7f21a0 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Tue, 13 Aug 2019 19:13:36 -0400 Subject: [PATCH 73/80] simplify model spec --- spec/unit/bootstrap/model_spec.rb | 67 +++---------------------------- 1 file changed, 5 insertions(+), 62 deletions(-) diff --git a/spec/unit/bootstrap/model_spec.rb b/spec/unit/bootstrap/model_spec.rb index 04320dfe8..9d6bf856f 100644 --- a/spec/unit/bootstrap/model_spec.rb +++ b/spec/unit/bootstrap/model_spec.rb @@ -9,69 +9,12 @@ deceasedDateTime: 'YYYY-MM-DD') end - it 'returns the root element (the resource)' do - element_definition = FHIR::ElementDefinition.new(id: 'Patient', path: 'Patient') - expect(resource.retrieve_elements_by_definition(element_definition)).to eq(element_definition.id => resource) - end - - it 'returns an attribute of element' do - element_definition = FHIR::ElementDefinition.new(id: 'Patient.id', path: 'Patient.id') - expect(resource.retrieve_elements_by_definition(element_definition)).to eq(element_definition.id => resource.id) - end - - context 'with choice of types' do - let(:element_definition) do - FHIR::ElementDefinition.new(id: 'Patient.deceased[x]', - path: 'Patient.deceased[x]', - type: [{ code: 'boolean' }, { code: 'dateTime' }]) - end - it 'returns all the elements present if there is a choice of type' do - titlecase_choice = [ - element_definition.id.gsub('[x]', - element_definition.type.first.code.capitalize), - element_definition.id.gsub('[x]', - "#{element_definition.type.last.code[0].capitalize}"\ - "#{element_definition.type.last.code[1..-1]}") - ] - expected_communications = { - titlecase_choice.first => resource.deceasedBoolean, - titlecase_choice.last => resource.deceasedDateTime - } - expect(resource.retrieve_elements_by_definition(element_definition)).to eq(expected_communications) - end - - it 'returns an array of all the elements present if there is a choice of type normalized to the element' do - expected_communications = { element_definition.id => [resource.deceasedBoolean, resource.deceasedDateTime] } - expect(resource.retrieve_elements_by_definition(element_definition, normalized: true)).to eq(expected_communications) - end - end - - context 'with sliced extensions' do - it 'returns all extensions' do - element_definition = FHIR::ElementDefinition.new(id: 'Patient.extension', path: 'Patient.extension') - expect(resource.retrieve_elements_by_definition(element_definition)).to eq(element_definition.id => resource.extension) - end - it 'returns the extensions indexed' do - expected_result = { 'Patient.extension[0]' => resource.extension[0], - 'Patient.extension[1]' => resource.extension[1] } - element_definition = FHIR::ElementDefinition.new(id: 'Patient.extension', path: 'Patient.extension') - expect(resource.retrieve_elements_by_definition(element_definition, indexed: true)).to eq(expected_result) - end - it 'returns the sliced extension' do - element_definition = FHIR::ElementDefinition.new(id: 'Patient.extension:foo', - path: 'Patient.extension', - sliceName: 'foo', - type: [{ code: 'Extension', profile: ['http://foo.org'] }]) - expect(resource.retrieve_elements_by_definition(element_definition)).to eq(element_definition.path => [resource.extension[1]]) - end + let(:element_definition) { FHIR::ElementDefinition.new(id: 'Patient', path: 'Patient') } - it 'returns the sliced extension indexed' do - element_definition = FHIR::ElementDefinition.new(id: 'Patient.extension:foo', - path: 'Patient.extension', - sliceName: 'foo', - type: [{ code: 'Extension', profile: ['http://foo.org'] }]) - expect(resource.retrieve_elements_by_definition(element_definition, indexed: true)).to eq("#{element_definition.path}[1]" => resource.extension[1]) - end + it 'delegates to the retriever module' do + allow(FHIR::Validation::Retrieval).to receive(:retrieve_by_element_definition) + resource.retrieve_elements_by_definition(element_definition) + expect(FHIR::Validation::Retrieval).to have_received(:retrieve_by_element_definition).with(resource, element_definition, hash_including(indexed: false, normalized: false)) end end end From 1f9f4ca6ba31cf702d31012e9e32f3b5c2dcb4ec Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Wed, 14 Aug 2019 08:21:23 -0400 Subject: [PATCH 74/80] cleanup structure validator test --- .../validation/structure_validator_spec.rb | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/spec/unit/validation/structure_validator_spec.rb b/spec/unit/validation/structure_validator_spec.rb index ae96e2ff4..6b310c9c0 100644 --- a/spec/unit/validation/structure_validator_spec.rb +++ b/spec/unit/validation/structure_validator_spec.rb @@ -1,12 +1,6 @@ describe FHIR::Validation::StructureValidator do - FIXTURES_DIR = File.join('test', 'fixtures') - - # let(:profile) {FHIR::StructureDefinition.new} - # let(:validator) do - # FHIR::Validation::StructureValidator.new(profile) - # end - # let(:profile) { FHIR::StructureDefinition.new(snapshot: { element: elements }) } + let(:elements) do [FHIR::ElementDefinition.new(id: 'Patient', path: 'Patient', @@ -31,16 +25,14 @@ end let(:validator) do - FHIR::Validation::StructureValidator.new(profile) + described_class.new(profile) end context 'with the default validators' do - before(:example) do - @results = validator.validate(resource) - end + let(:results) { validator.validate(resource) } + it 'returns cardinality results' do - cardinality_results = @results.select { |res| res.validation_type == :cardinality } - expect(cardinality_results.length.positive?).to be_truthy + expect(results.select { |res| res.validation_type == :cardinality }).not_to be_empty end end end From b411128f9892903d0899d48645241d8cfc1ea8cd Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Wed, 14 Aug 2019 09:12:40 -0400 Subject: [PATCH 75/80] cleaning up terminology validator spec --- .../terminology_validator_spec.rb | 185 ++++++++++-------- 1 file changed, 102 insertions(+), 83 deletions(-) diff --git a/spec/unit/validation/element_validator/terminology_validator_spec.rb b/spec/unit/validation/element_validator/terminology_validator_spec.rb index d8f703539..d5740d86d 100644 --- a/spec/unit/validation/element_validator/terminology_validator_spec.rb +++ b/spec/unit/validation/element_validator/terminology_validator_spec.rb @@ -1,39 +1,58 @@ describe FHIR::Validation::TerminologyValidator do + data_types = %w[CodeableConcept Quantity Coding] + bindings = %w[required extensible preferred example] + + data_binding_pairs = data_types.product(bindings) + + validation_options = [true, false] + validation_combinations = Array.new(3, validation_options) + let(:in_system) do lambda do true end end - data_types = %w[CodeableConcept Quantity Coding] - bindings = %w[required extensible preferred example] + let(:validator) { described_class.new } - data_binding_pairs = data_types.product(bindings) + it 'contains no validators initially' do + expect(validator.vs_validators).to be_empty + end - validation_options = [true, false] - validation_combinations = Array.new(3, validation_options) + it 'allows additional validators to be added' do + vs_validator = { 'foo' => ->(_coding) { true } } + validator.register_vs_validator(vs_validator.keys.first, vs_validator[vs_validator.keys.first]) + expect(validator.vs_validators).to eq(vs_validator) + end + + it 'allows validators to be removed' do + vs_validator = { 'foo' => ->(_coding) { true } } + validator.register_vs_validator(vs_validator.keys.first, vs_validator[vs_validator.keys.first]) + validator.clear_vs_validators + expect(validator.vs_validators).to be_empty + end - shared_context 'ValueSet is known' do |known| + shared_context 'when ValueSet is known' do |known| known ? let(:value_set) { 'foo' } : let(:value_set) { 'bar' } end - shared_context 'CodeSystem is known' do |known| + shared_context 'when CodeSystem is known' do |known| known ? let(:code_system) { 'qux' } : let(:code_system) { 'corge' } end - shared_context 'code is in the ValueSet' do |included| + shared_context 'when code is in the ValueSet' do |included| let(:value_set_validator) do { 'foo' => ->(_coding) { included } } end end - shared_context 'code is in the CodeSystem' do |included| + shared_context 'when code is in the CodeSystem' do |included| let(:code_system_validator) do { 'qux' => ->(_coding) { included } } end end - shared_context 'Resource type' do |resource| + shared_context 'with Resource type' do |resource| let(:resource) do case resource when 'CodeableConcept' @@ -46,7 +65,7 @@ end end - shared_context 'ElementDefinition' do |binding_strength, type, second_type = nil| + shared_context 'with ElementDefinition' do |binding_strength, type, second_type = nil| let(:element_definition) do types = [{ code: type }] types.push(code: second_type) if second_type @@ -57,117 +76,117 @@ end end - let(:validator) { FHIR::Validation::TerminologyValidator.new(value_set_validator.merge(code_system_validator)) } + shared_examples 'terminology results' do + it 'contains terminology results' do + expect(results).to all(have_attributes(validation_type: :terminology)) + end + end + + shared_examples 'results status' do |status| + it 'has the expected result' do + expect(results).to all(have_attributes(result: status)) + end + end + + shared_examples 'expected number of results' do |number| + it 'returns the expected number of results' do + expect(results.size).to eq(number) + end + end + + shared_examples 'result includes text containing' do |text| + it 'result includes expected text' do + expect(results).to all(have_attributes(text: a_string_including(text))) + end + end + + shared_examples 'code or system missing from element' do |number_of_results, included_text| + include_examples 'terminology results' + include_examples 'results status', :warn + include_examples 'expected number of results', number_of_results + include_examples 'result includes text containing', included_text + end shared_examples 'expected results' do |validation_combo, binding_strength| required_strength = (binding_strength == 'required') fail_strength = required_strength ? :fail : :warn it 'returns two results' do - expect(@results.size).to eq(2) + expect(results.size).to eq(2) end if validation_combo[0] it 'checks for inclusion in the ValueSet' do - expect(@results.first).to have_attributes(validation_type: :terminology) - expect(@results.first).to have_attributes(text: a_string_including("codings from #{value_set}")) - expect(@results.first).to have_attributes(result: (validation_combo[2] ? :pass : fail_strength)) + expect(results.first).to have_attributes(result: (validation_combo[2] ? :pass : fail_strength), + text: a_string_including("codings from #{value_set}")) end else it 'warns that the ValueSet validator is missing' do - expect(@results.first).to have_attributes(validation_type: :terminology) - expect(@results.first).to have_attributes(text: a_string_including('Missing Validator for bar')) - expect(@results.first).to have_attributes(result: :warn) + expect(results.first).to have_attributes(result: :warn, + text: a_string_including('Missing Validator for bar')) end end if validation_combo[1] it 'checks for inclusion in the CodeSystem' do - expect(@results.last).to have_attributes(validation_type: :terminology) - expect(@results.last).to have_attributes(text: a_string_including("codings from #{code_system}")) - expect(@results.last).to have_attributes(result: (validation_combo[3] ? :pass : :fail)) + expect(results.last).to have_attributes(result: (validation_combo[3] ? :pass : :fail), + text: a_string_including("codings from #{code_system}")) end else it 'warns that the CodeSystem validator is missing' do - expect(@results.last).to have_attributes(validation_type: :terminology) - expect(@results.last).to have_attributes(text: a_string_including('Missing Validator for corge')) - expect(@results.last).to have_attributes(result: :warn) + expect(results.last).to have_attributes(result: :warn, + text: a_string_including('Missing Validator for corge')) end end + + it 'includes terminology results' do + expect(results).to all(have_attributes(validation_type: :terminology)) + end end - context 'with no code or valueset validators' do - include_context 'code is in the ValueSet' - let(:validator) { FHIR::Validation::TerminologyValidator.new } - it 'contains no validators initially' do - expect(validator.vs_validators).to be_empty + describe '#validate' do + let(:element_definition) do + FHIR::ElementDefinition.new(id: 'Element', + path: 'Element', + binding: { strength: 'required', valueSet: 'asdf' }, + type: [{ code: 'CodeableConcept' }]) end - it 'allows additional validators to be added and removed' do - validator.register_vs_validator(value_set_validator.keys.first, value_set_validator[value_set_validator.keys.first]) - expect(validator.vs_validators).to eq(value_set_validator) + let(:results) { validator.validate(resource, element_definition) } - validator.clear_vs_validators - expect(validator.vs_validators).to be_empty - end + context 'when the system is missing from the element' do + let(:resource) { FHIR::CodeableConcept.new(coding: { code: 'waldo' }) } - it 'warns if the code is missing' do - results = validator.validate(FHIR::CodeableConcept.new(coding: { system: 'adsf' }), - FHIR::ElementDefinition.new(id: 'Element', - path: 'Element', - binding: { strength: 'required', valueSet: 'asdf' }, - type: [{ code: 'CodeableConcept' }])) - expect(results.size).to eq(1) - expect(results).to all(have_attributes(validation_type: :terminology)) - expect(results).to all(have_attributes(text: a_string_including('missing code'))) - expect(results).to all(have_attributes(result: :warn)) + include_examples 'code or system missing from element', 1, 'missing system' end - it 'warns if the system is missing' do - results = validator.validate(FHIR::CodeableConcept.new(coding: { code: 'waldo' }), - FHIR::ElementDefinition.new(id: 'Element', - path: 'Element', - binding: { strength: 'required', valueSet: 'asdf' }, - type: [{ code: 'CodeableConcept' }])) + context 'when the code is missing from the element' do + let(:resource) { FHIR::CodeableConcept.new(coding: { system: 'adsf' }) } - expect(results.size).to eq(1) - expect(results).to all(have_attributes(validation_type: :terminology)) - expect(results).to all(have_attributes(text: a_string_including('missing system'))) - expect(results).to all(have_attributes(result: :warn)) + include_examples 'code or system missing from element', 1, 'missing code' end - it 'warns if the code and system are missing' do - results = validator.validate(FHIR::CodeableConcept.new(coding: { text: 'nope' }), - FHIR::ElementDefinition.new(id: 'Element', - path: 'Element', - binding: { strength: 'required', valueSet: 'asdf' }, - type: [{ code: 'CodeableConcept' }])) + context 'when both the code and system are missing from the element' do + let(:resource) { FHIR::CodeableConcept.new(coding: { text: 'nope' }) } - expect(results.size).to eq(2) - expect(results).to all(have_attributes(validation_type: :terminology)) - expect(results).to all(have_attributes(text: a_string_including('missing'))) - expect(results).to all(have_attributes(result: :warn)) + include_examples 'code or system missing from element', 2, 'missing' end - end - describe '#validate' do [nil, 'FakeType'].each do |secondary_type| data_binding_pairs.each do |data_binding_pair| - context "with a #{data_binding_pair[0]}, #{data_binding_pair[1]} binding, " do - include_context 'ElementDefinition', data_binding_pair[1], data_binding_pair[0], secondary_type - include_context 'Resource type', data_binding_pair[0] - validation_combinations.each do |combo| - context "#{'un' unless combo[0]}known ValueSet #{'not ' unless combo[2]}containing the code, and "\ - "#{'un' unless combo[1]}known CodeSystem #{'not ' unless combo[3]}containing the code" do - include_context 'ValueSet is known', combo[0] - include_context 'CodeSystem is known', combo[1] - include_context 'code is in the ValueSet', combo[2] - include_context 'code is in the CodeSystem', combo[3] - before(:example) do - @results = validator.validate(resource, element_definition) - end - include_examples 'expected results', combo, data_binding_pair[1] - end + validation_combinations.each do |combo| + context "with a #{data_binding_pair[0]}, #{data_binding_pair[1]} binding, #{'un' unless combo[0]}known ValueSet #{'not ' unless combo[2]}containing the code, and #{'un' unless combo[1]}known CodeSystem #{'not ' unless combo[3]}containing the code" do + include_context 'with ElementDefinition', data_binding_pair[1], data_binding_pair[0], secondary_type + include_context 'with Resource type', data_binding_pair[0] + include_context 'when ValueSet is known', combo[0] + include_context 'when CodeSystem is known', combo[1] + include_context 'when code is in the ValueSet', combo[2] + include_context 'when code is in the CodeSystem', combo[3] + + let(:validator) { described_class.new(value_set_validator.merge(code_system_validator)) } + let(:results) { validator.validate(resource, element_definition) } + + include_examples 'expected results', combo, data_binding_pair[1] end end end From cfc1abb416bea8a7f056532924bee091a3a333a3 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Wed, 14 Aug 2019 09:17:48 -0400 Subject: [PATCH 76/80] minor updates and regenerate todo --- .rubocop_todo.yml | 130 +++++++++++++----- spec/unit/fhir_ext/element_definition_spec.rb | 12 +- .../data_type_validator_spec.rb | 6 +- 3 files changed, 104 insertions(+), 44 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 8ea06afad..e5c2329d7 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,24 +1,21 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2019-04-22 22:20:52 -0400 using RuboCop version 0.67.0. +# on 2019-08-14 09:13:47 -0400 using RuboCop version 0.67.2. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 51 +# Offense count: 40 # Cop supports --auto-correct. Layout/EmptyLineAfterGuardClause: Exclude: - 'lib/fhir_models/bootstrap/definitions.rb' - 'lib/fhir_models/bootstrap/generator.rb' - 'lib/fhir_models/bootstrap/hashable.rb' - - 'lib/fhir_models/bootstrap/model.rb' - 'lib/fhir_models/bootstrap/preprocess.rb' - 'lib/fhir_models/bootstrap/xml.rb' - 'lib/fhir_models/deprecate.rb' - - 'lib/fhir_models/fhir_ext/element_definition.rb' - - 'lib/fhir_models/fhir_ext/structure_definition.rb' - 'lib/fhir_models/fhir_ext/structure_definition_compare.rb' - 'lib/fhir_models/fluentpath/parse.rb' @@ -35,53 +32,49 @@ Lint/ToJSON: - 'lib/fhir_models/bootstrap/json.rb' - 'lib/fhir_models/fhir_ext/structure_definition_finding.rb' -# Offense count: 40 +# Offense count: 34 Metrics/AbcSize: Max: 392 -# Offense count: 7 +# Offense count: 5 # Configuration parameters: CountComments, ExcludedMethods. # ExcludedMethods: refine Metrics/BlockLength: - Max: 74 + Max: 68 -# Offense count: 8 +# Offense count: 1 # Configuration parameters: CountBlocks. Metrics/BlockNesting: - Max: 5 + Max: 4 -# Offense count: 7 +# Offense count: 6 # Configuration parameters: CountComments. Metrics/ClassLength: - Max: 355 + Max: 269 -# Offense count: 23 +# Offense count: 16 Metrics/CyclomaticComplexity: Max: 65 -# Offense count: 44 +# Offense count: 37 # Configuration parameters: CountComments, ExcludedMethods. Metrics/MethodLength: - Max: 131 + Max: 109 -# Offense count: 2 +# Offense count: 3 # Configuration parameters: CountComments. Metrics/ModuleLength: Max: 114 -# Offense count: 23 +# Offense count: 15 Metrics/PerceivedComplexity: Max: 68 -# Offense count: 5 +# Offense count: 1 # Configuration parameters: PreferredName. Naming/RescuedExceptionsVariableName: Exclude: - 'lib/fhir_models/bootstrap/hashable.rb' - - 'lib/fhir_models/bootstrap/json.rb' - - 'lib/fhir_models/bootstrap/xml.rb' - - 'lib/fhir_models/fhir_ext/structure_definition.rb' - - 'lib/fhir_models/fhir_ext/structure_definition_finding.rb' # Offense count: 4 # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. @@ -102,7 +95,81 @@ Performance/InefficientHashSearch: Exclude: - 'lib/fhir_models/bootstrap/template.rb' -# Offense count: 1 +# Offense count: 31 +# Configuration parameters: Prefixes. +# Prefixes: when, with, without +RSpec/ContextWording: + Exclude: + - 'spec/lib/fhir_models/fhir_spec.rb' + - 'spec/unit/bootstrap/definitions_spec.rb' + +# Offense count: 3 +RSpec/DescribeClass: + Exclude: + - 'spec/lib/fhir_models/bootstrap/model_spec.rb' + - 'spec/lib/fhir_models/fhir_ext/structure_definition_spec.rb' + - 'spec/lib/fhir_models/fhir_spec.rb' + +# Offense count: 9 +# Cop supports --auto-correct. +# Configuration parameters: SkipBlocks, EnforcedStyle. +# SupportedStyles: described_class, explicit +RSpec/DescribedClass: + Exclude: + - 'spec/unit/bootstrap/definitions_spec.rb' + +# Offense count: 2 +# Configuration parameters: CustomIncludeMethods. +RSpec/EmptyExampleGroup: + Exclude: + - 'spec/lib/fhir_models/fhir_ext/structure_definition_spec.rb' + +# Offense count: 4 +# Cop supports --auto-correct. +RSpec/EmptyLineAfterExampleGroup: + Exclude: + - 'spec/lib/fhir_models/fhir_spec.rb' + +# Offense count: 31 +# Cop supports --auto-correct. +RSpec/EmptyLineAfterFinalLet: + Exclude: + - 'spec/lib/fhir_models/fhir_spec.rb' + +# Offense count: 6 +# Configuration parameters: Max. +RSpec/ExampleLength: + Exclude: + - 'spec/lib/fhir_models/bootstrap/model_spec.rb' + +# Offense count: 6 +# Cop supports --auto-correct. +# Configuration parameters: CustomTransform, IgnoredWords. +RSpec/ExampleWording: + Exclude: + - 'spec/lib/fhir_models/bootstrap/model_spec.rb' + +# Offense count: 5 +# Configuration parameters: AggregateFailuresByDefault. +RSpec/MultipleExpectations: + Max: 3 + +# Offense count: 121 +# Configuration parameters: IgnoreSharedExamples. +RSpec/NamedSubject: + Exclude: + - 'spec/lib/fhir_models/fhir_spec.rb' + +# Offense count: 28 +RSpec/NestedGroups: + Max: 4 + +# Offense count: 2 +RSpec/RepeatedDescription: + Exclude: + - 'spec/lib/fhir_models/fhir_spec.rb' + +# Offense count: 25 # Configuration parameters: . # SupportedStyles: inline, group Style/AccessModifierDeclarations: @@ -114,11 +181,10 @@ Style/ClassVars: - 'lib/fhir_models/bootstrap/definitions.rb' - 'lib/fhir_models/fluentpath/parse.rb' -# Offense count: 6 +# Offense count: 4 Style/CommentedKeyword: Exclude: - 'lib/fhir_models/bootstrap/hashable.rb' - - 'lib/fhir_models/bootstrap/model.rb' # Offense count: 14 Style/Documentation: @@ -140,7 +206,7 @@ Style/Documentation: - 'lib/fhir_models/fluentpath/expression.rb' - 'lib/fhir_models/fluentpath/parse.rb' -# Offense count: 29 +# Offense count: 24 # Cop supports --auto-correct. Style/IfUnlessModifier: Exclude: @@ -149,7 +215,6 @@ Style/IfUnlessModifier: - 'lib/fhir_models/bootstrap/model.rb' - 'lib/fhir_models/bootstrap/template.rb' - 'lib/fhir_models/bootstrap/xml.rb' - - 'lib/fhir_models/fhir_ext/structure_definition.rb' - 'lib/fhir_models/fhir_ext/structure_definition_compare.rb' # Offense count: 1 @@ -162,13 +227,11 @@ Style/MissingRespondToMissing: Exclude: - 'lib/fhir_models/bootstrap/model.rb' -# Offense count: 9 +# Offense count: 4 Style/MultipleComparison: Exclude: - 'lib/fhir_models/bootstrap/definitions.rb' - 'lib/fhir_models/bootstrap/generator.rb' - - 'lib/fhir_models/bootstrap/model.rb' - - 'lib/fhir_models/fhir_ext/structure_definition.rb' # Offense count: 2 # Cop supports --auto-correct. @@ -185,7 +248,7 @@ Style/OrAssignment: Exclude: - 'lib/fhir_models/bootstrap/generator.rb' -# Offense count: 16 +# Offense count: 10 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle. # SupportedStyles: implicit, explicit @@ -194,15 +257,13 @@ Style/RescueStandardError: - 'lib/fhir_models/bootstrap/definitions.rb' - 'lib/fhir_models/bootstrap/hashable.rb' - 'lib/fhir_models/bootstrap/json.rb' - - 'lib/fhir_models/bootstrap/model.rb' - 'lib/fhir_models/bootstrap/xml.rb' - 'lib/fhir_models/fhir.rb' - - 'lib/fhir_models/fhir_ext/structure_definition.rb' - 'lib/fhir_models/fhir_ext/structure_definition_finding.rb' - 'lib/fhir_models/fluentpath/expression.rb' - 'lib/fhir_models/fluentpath/parse.rb' -# Offense count: 6 +# Offense count: 3 # Cop supports --auto-correct. # Configuration parameters: ConvertCodeThatCanStartToReturnNil, Whitelist. # Whitelist: present?, blank?, presence, try, try! @@ -210,7 +271,6 @@ Style/SafeNavigation: Exclude: - 'lib/fhir_models/bootstrap/definitions.rb' - 'lib/fhir_models/bootstrap/xml.rb' - - 'lib/fhir_models/fhir_ext/structure_definition.rb' # Offense count: 7 # Cop supports --auto-correct. diff --git a/spec/unit/fhir_ext/element_definition_spec.rb b/spec/unit/fhir_ext/element_definition_spec.rb index c68a448d3..321612464 100644 --- a/spec/unit/fhir_ext/element_definition_spec.rb +++ b/spec/unit/fhir_ext/element_definition_spec.rb @@ -12,14 +12,14 @@ end describe '#choice_type' do context 'with a choice of types' do - include_context 'choice of type' + include_context 'with a choice of type' it 'indicates there is a choice of type' do expect(element_definition.choice_type?).to be true end end context 'with a single type' do - include_context 'single type' + include_context 'with a single type' it 'indicates there is not a choice of types' do expect(element_definition.choice_type?).to be false end @@ -28,7 +28,7 @@ describe '#type_code' do context 'with a single type' do - include_context 'single type' + include_context 'with a single type' it 'provides the expected type of the element when provided a path' do expect(element_definition.type_code('fake.path')).to eq('boolean') end @@ -39,7 +39,7 @@ end context 'with a choice of types' do - include_context 'choice of type' + include_context 'with a choice of type' it 'provides the correct type from the choices when the first type is provided' do expect(element_definition.type_code('Patient.deceasedBoolean')).to eq('boolean') end @@ -61,14 +61,14 @@ describe '#type_paths' do context 'with a single type' do - include_context 'single type' + include_context 'with a single type' it 'returns a single path option' do expect(element_definition.type_paths).to eq(['Patient.deceased']) end end context 'with a choice of types' do - include_context 'choice of type' + include_context 'with a choice of type' it 'returns a path for each type' do expect(element_definition.type_paths).to eq(['Patient.deceasedBoolean', 'Patient.deceasedDateTime']) end diff --git a/spec/unit/validation/element_validator/data_type_validator_spec.rb b/spec/unit/validation/element_validator/data_type_validator_spec.rb index 947c8b738..f2c8d6ac5 100644 --- a/spec/unit/validation/element_validator/data_type_validator_spec.rb +++ b/spec/unit/validation/element_validator/data_type_validator_spec.rb @@ -36,7 +36,7 @@ type: { code: 'Patient' }) end - include_context 'results' + include_context 'with results' it 'skips the root element' do results = validator.validate(resource, element_definition) @@ -61,7 +61,7 @@ type: { code: 'Foo' }) end - include_context 'results' + include_context 'with results' include_examples 'data type result' @@ -97,7 +97,7 @@ [type1, type2] end - include_context 'results' + include_context 'with results' include_examples 'data type result' include_examples 'cardinality results' From f2b048959fb4f4c307835673f2089b94bd5d0896 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Wed, 14 Aug 2019 10:52:39 -0400 Subject: [PATCH 77/80] simplify terminology tests --- .../terminology_validator_spec.rb | 191 ++++++++---------- 1 file changed, 80 insertions(+), 111 deletions(-) diff --git a/spec/unit/validation/element_validator/terminology_validator_spec.rb b/spec/unit/validation/element_validator/terminology_validator_spec.rb index d5740d86d..2c176cea1 100644 --- a/spec/unit/validation/element_validator/terminology_validator_spec.rb +++ b/spec/unit/validation/element_validator/terminology_validator_spec.rb @@ -1,18 +1,4 @@ describe FHIR::Validation::TerminologyValidator do - data_types = %w[CodeableConcept Quantity Coding] - bindings = %w[required extensible preferred example] - - data_binding_pairs = data_types.product(bindings) - - validation_options = [true, false] - validation_combinations = Array.new(3, validation_options) - - let(:in_system) do - lambda do - true - end - end - let(:validator) { described_class.new } it 'contains no validators initially' do @@ -32,50 +18,6 @@ expect(validator.vs_validators).to be_empty end - shared_context 'when ValueSet is known' do |known| - known ? let(:value_set) { 'foo' } : let(:value_set) { 'bar' } - end - - shared_context 'when CodeSystem is known' do |known| - known ? let(:code_system) { 'qux' } : let(:code_system) { 'corge' } - end - - shared_context 'when code is in the ValueSet' do |included| - let(:value_set_validator) do - { 'foo' => ->(_coding) { included } } - end - end - - shared_context 'when code is in the CodeSystem' do |included| - let(:code_system_validator) do - { 'qux' => ->(_coding) { included } } - end - end - - shared_context 'with Resource type' do |resource| - let(:resource) do - case resource - when 'CodeableConcept' - FHIR::CodeableConcept.new(coding: { code: 'waldo', system: code_system }) - when 'Quantity' - FHIR::Quantity.new(code: 'waldo', system: code_system) - when 'Coding' - FHIR::Coding.new(code: 'waldo', system: code_system) - end - end - end - - shared_context 'with ElementDefinition' do |binding_strength, type, second_type = nil| - let(:element_definition) do - types = [{ code: type }] - types.push(code: second_type) if second_type - FHIR::ElementDefinition.new(id: "Element#{'[x]' if second_type}", - path: "Element#{'[x]' if second_type}", - binding: { strength: binding_strength, valueSet: value_set }, - type: types) - end - end - shared_examples 'terminology results' do it 'contains terminology results' do expect(results).to all(have_attributes(validation_type: :terminology)) @@ -107,51 +49,16 @@ include_examples 'result includes text containing', included_text end - shared_examples 'expected results' do |validation_combo, binding_strength| - required_strength = (binding_strength == 'required') - fail_strength = required_strength ? :fail : :warn - - it 'returns two results' do - expect(results.size).to eq(2) - end - - if validation_combo[0] - it 'checks for inclusion in the ValueSet' do - expect(results.first).to have_attributes(result: (validation_combo[2] ? :pass : fail_strength), - text: a_string_including("codings from #{value_set}")) - end - else - it 'warns that the ValueSet validator is missing' do - expect(results.first).to have_attributes(result: :warn, - text: a_string_including('Missing Validator for bar')) - end - end - - if validation_combo[1] - it 'checks for inclusion in the CodeSystem' do - expect(results.last).to have_attributes(result: (validation_combo[3] ? :pass : :fail), - text: a_string_including("codings from #{code_system}")) - end - else - it 'warns that the CodeSystem validator is missing' do - expect(results.last).to have_attributes(result: :warn, - text: a_string_including('Missing Validator for corge')) - end - end - - it 'includes terminology results' do - expect(results).to all(have_attributes(validation_type: :terminology)) - end - end - describe '#validate' do let(:element_definition) do FHIR::ElementDefinition.new(id: 'Element', path: 'Element', - binding: { strength: 'required', valueSet: 'asdf' }, + binding: { strength: 'required', valueSet: 'foo' }, type: [{ code: 'CodeableConcept' }]) end + let(:resource) { FHIR::CodeableConcept.new(coding: { code: 'waldo', system: 'qux' }) } + let(:results) { validator.validate(resource, element_definition) } context 'when the system is missing from the element' do @@ -172,23 +79,85 @@ include_examples 'code or system missing from element', 2, 'missing' end - [nil, 'FakeType'].each do |secondary_type| - data_binding_pairs.each do |data_binding_pair| - validation_combinations.each do |combo| - context "with a #{data_binding_pair[0]}, #{data_binding_pair[1]} binding, #{'un' unless combo[0]}known ValueSet #{'not ' unless combo[2]}containing the code, and #{'un' unless combo[1]}known CodeSystem #{'not ' unless combo[3]}containing the code" do - include_context 'with ElementDefinition', data_binding_pair[1], data_binding_pair[0], secondary_type - include_context 'with Resource type', data_binding_pair[0] - include_context 'when ValueSet is known', combo[0] - include_context 'when CodeSystem is known', combo[1] - include_context 'when code is in the ValueSet', combo[2] - include_context 'when code is in the CodeSystem', combo[3] + context 'when the ValueSet is missing or unknown' do + it 'warns that the ValueSet validator is missing' do + expect(results).to include(have_attributes(result: :warn, + text: a_string_including('Missing Validator for foo'))) + end + end + + context 'when the CodeSystem is missing or unkown' do + it 'warns that the CodeSystem validator is missing' do + expect(results).to include(have_attributes(result: :warn, + text: a_string_including('Missing Validator for qux'))) + end + end + + context 'when the code is in the ValueSet' do + let(:validator) { described_class.new('foo' => ->(_coding) { true }) } + + it 'indicates that the code is in the ValueSet' do + expect(results).to include(have_attributes(result: :pass, + text: a_string_including('codings from foo'))) + end + end + + context 'when the code is missing from the ValueSet' do + let(:validator) { described_class.new('foo' => ->(_coding) { false }) } - let(:validator) { described_class.new(value_set_validator.merge(code_system_validator)) } - let(:results) { validator.validate(resource, element_definition) } + it 'indicates that the code is in the ValueSet' do + expect(results).to include(have_attributes(result: :fail, + text: a_string_including('codings from foo'))) + end + end + + context 'when the code is in the CodeSystem' do + let(:validator) { described_class.new('qux' => ->(_coding) { true }) } + + it 'indicates that the code is in the CodeSystem' do + expect(results).to include(have_attributes(result: :pass, + text: a_string_including('codings from qux'))) + end + end + + context 'when the code is missing from the CodeSystem' do + let(:validator) { described_class.new('qux' => ->(_coding) { false }) } + + it 'indicates that the code is in the ValueSet' do + expect(results).to include(have_attributes(result: :fail, + text: a_string_including('codings from qux'))) + end + end + + context 'when the element is Quantity' do + let(:resource) { FHIR::Quantity.new(code: 'waldo', system: 'foo') } + let(:validator) { described_class.new('foo' => ->(_coding) { true }) } + let(:element_definition) do + FHIR::ElementDefinition.new(id: 'Element', + path: 'Element', + binding: { strength: 'required', valueSet: 'foo' }, + type: [{ code: 'Quantity' }]) + end + + it 'indicates that the code is in the ValueSet' do + expect(results).to include(have_attributes(result: :pass, + text: a_string_including('codings from foo'))) + end + end + + context 'when the element is Coding' do + let(:resource) { FHIR::Coding.new(code: 'waldo', system: 'foo') } + let(:validator) { described_class.new('foo' => ->(_coding) { true }) } + let(:element_definition) do + FHIR::ElementDefinition.new(id: 'Element', + path: 'Element', + binding: { strength: 'required', valueSet: 'foo' }, + type: [{ code: 'Coding' }]) + end - include_examples 'expected results', combo, data_binding_pair[1] - end - end + it 'indicates that the code is in the ValueSet' do + expect(results).to include(have_attributes(result: :pass, + text: a_string_including('codings from foo'))) end end end From 62f1e2073ffbbd88397fa32ca55b5af3fc88734c Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Wed, 14 Aug 2019 10:55:27 -0400 Subject: [PATCH 78/80] remove a few unnecessary tests from retrieval --- spec/unit/validation/retrieval_spec.rb | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/spec/unit/validation/retrieval_spec.rb b/spec/unit/validation/retrieval_spec.rb index 81b2e6d06..953652654 100644 --- a/spec/unit/validation/retrieval_spec.rb +++ b/spec/unit/validation/retrieval_spec.rb @@ -57,16 +57,6 @@ end context 'with sliced extensions' do - it 'returns all extensions' do - element_definition = FHIR::ElementDefinition.new(id: 'Patient.extension', path: 'Patient.extension') - expect(retrieval.retrieve_by_element_definition(resource, element_definition)).to eq(element_definition.id => resource.extension) - end - it 'returns the extensions indexed' do - expected_result = { 'Patient.extension[0]' => resource.extension[0], - 'Patient.extension[1]' => resource.extension[1] } - element_definition = FHIR::ElementDefinition.new(id: 'Patient.extension', path: 'Patient.extension') - expect(retrieval.retrieve_by_element_definition(resource, element_definition, indexed: true)).to eq(expected_result) - end it 'returns the sliced extension' do element_definition = FHIR::ElementDefinition.new(id: 'Patient.extension:foo', path: 'Patient.extension', From 11d69c051dbd05df58d18246b0e1dbb270eafd76 Mon Sep 17 00:00:00 2001 From: Reece Adamson Date: Thu, 15 Aug 2019 09:30:14 -0400 Subject: [PATCH 79/80] rename variable path_path to path_part --- lib/fhir_models/validation/structure_validator.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/fhir_models/validation/structure_validator.rb b/lib/fhir_models/validation/structure_validator.rb index 0aeb00ea1..2c03629c5 100644 --- a/lib/fhir_models/validation/structure_validator.rb +++ b/lib/fhir_models/validation/structure_validator.rb @@ -58,9 +58,9 @@ def add_default_element_validators end private def add_element_path_to_hierarchy(hierarchy, path) - path.reduce(hierarchy) do |hierarchy_memo, path_path| - hierarchy_memo[path_path] ||= hierarchy_node - hierarchy_memo[path_path][:path] + path.reduce(hierarchy) do |hierarchy_memo, path_part| + hierarchy_memo[path_part] ||= hierarchy_node + hierarchy_memo[path_part][:path] end end From a291c37d1a7bd7412b02c14f15de4041aa3e0195 Mon Sep 17 00:00:00 2001 From: Jason Walonoski Date: Wed, 18 Sep 2019 09:04:10 -0400 Subject: [PATCH 80/80] Fix validation of multiple types with latest US Core definitions. --- lib/fhir_models/validation/retrieval.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/fhir_models/validation/retrieval.rb b/lib/fhir_models/validation/retrieval.rb index e3032c405..e62ad1954 100644 --- a/lib/fhir_models/validation/retrieval.rb +++ b/lib/fhir_models/validation/retrieval.rb @@ -36,6 +36,7 @@ def self.path_parts(path) # @param structure [FHIR::Model] the structure from which the element will be retrieved def self.retrieve_from_structure(element, structure) fixed_name = %w[class method resourceType].include?(element) ? "local_#{element}" : element + fixed_name = fixed_name.gsub('[x]','') structure.send(fixed_name) if structure.is_a? FHIR::Model # FHIR Primitives are not modeled and will throw NoMethod Error end