fhir_models 1.8.2 → 1.8.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +1 -0
- data/.rspec +2 -0
- data/.rubocop.yml +5 -1156
- data/.rubocop_todo.yml +76 -0
- data/.travis.yml +9 -3
- data/Gemfile +1 -1
- data/Guardfile +50 -0
- data/Rakefile +4 -5
- data/bin/console +4 -4
- data/fhir_models.gemspec +6 -3
- data/lib/fhir_models.rb +2 -1
- data/lib/fhir_models/bootstrap/definitions.rb +100 -94
- data/lib/fhir_models/bootstrap/field.rb +1 -1
- data/lib/fhir_models/bootstrap/generator.rb +11 -11
- data/lib/fhir_models/bootstrap/hashable.rb +18 -5
- data/lib/fhir_models/bootstrap/json.rb +2 -1
- data/lib/fhir_models/bootstrap/model.rb +21 -39
- data/lib/fhir_models/bootstrap/preprocess.rb +21 -36
- data/lib/fhir_models/bootstrap/template.rb +5 -9
- data/lib/fhir_models/bootstrap/xml.rb +8 -5
- data/lib/fhir_models/deprecate.rb +22 -0
- data/lib/fhir_models/fhir.rb +58 -0
- data/lib/fhir_models/fhir/metadata.rb +1 -1
- data/lib/fhir_models/fhir_ext/element_definition.rb +47 -0
- data/lib/fhir_models/fhir_ext/structure_definition.rb +203 -524
- data/lib/fhir_models/fhir_ext/structure_definition_compare.rb +375 -0
- data/lib/fhir_models/fluentpath/evaluate.rb +9 -2
- data/lib/fhir_models/fluentpath/expression.rb +8 -1
- data/lib/fhir_models/fluentpath/parse.rb +2 -2
- data/lib/fhir_models/version.rb +1 -1
- metadata +62 -15
- data/lib/fhir_models/fhir/resources/MetadataResource.rb +0 -52
@@ -0,0 +1,22 @@
|
|
1
|
+
module FHIR
|
2
|
+
# add support for deprecating instance and class methods
|
3
|
+
module Deprecate
|
4
|
+
def deprecate(old_method, new_method)
|
5
|
+
if instance_methods.include? new_method
|
6
|
+
define_method(old_method) do |*args, &block|
|
7
|
+
message = "DEPRECATED: `#{old_method}` has been deprecated. Use `#{new_method}` instead. Called from #{caller.first}"
|
8
|
+
FHIR.logger.warn message
|
9
|
+
send(new_method, *args, &block)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
return unless methods.include? new_method
|
13
|
+
(class << self; self; end).instance_eval do
|
14
|
+
define_method(old_method) do |*args, &block|
|
15
|
+
message = "DEPRECATED: `#{old_method}` has been deprecated. Use `#{new_method}` instead. Called from #{caller.first}"
|
16
|
+
FHIR.logger.warn message
|
17
|
+
send(new_method, *args, &block)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/fhir_models/fhir.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'nokogiri'
|
2
2
|
require 'logger'
|
3
|
+
|
3
4
|
module FHIR
|
4
5
|
def self.logger
|
5
6
|
@logger || default_logger
|
@@ -21,4 +22,61 @@ module FHIR
|
|
21
22
|
FHIR::Json.from_json(contents)
|
22
23
|
end
|
23
24
|
end
|
25
|
+
|
26
|
+
# TODO: pull regexes from metadata
|
27
|
+
def self.primitive?(datatype:, value:)
|
28
|
+
# Remaining data types: handle special cases before checking type StructureDefinitions
|
29
|
+
case datatype.downcase
|
30
|
+
when 'boolean'
|
31
|
+
!(value.to_s =~ /\A(true|false)\Z/).nil?
|
32
|
+
when 'integer'
|
33
|
+
!(value.to_s =~ /\A(0|[-+]?[1-9][0-9]*)\Z/).nil?
|
34
|
+
when 'string', 'markdown'
|
35
|
+
value.is_a?(String)
|
36
|
+
when 'decimal'
|
37
|
+
!(value.to_s =~ /\A([-+]?([0]|([1-9][0-9]*))(\.[0-9]+)?)\Z/).nil?
|
38
|
+
when 'uri'
|
39
|
+
begin
|
40
|
+
!URI.parse(value).nil?
|
41
|
+
rescue
|
42
|
+
false
|
43
|
+
end
|
44
|
+
when 'base64binary'
|
45
|
+
# According to RFC-4648 base64binary encoding includes digits 0-9, a-z, A-Z, =, +, /, and whitespace
|
46
|
+
# an empty string is considered valid
|
47
|
+
# whitespace is not significant so we strip it out before doing the regex so that we can be sure that
|
48
|
+
# the number of characters is a multiple of 4.
|
49
|
+
# https://tools.ietf.org/html/rfc4648
|
50
|
+
!(value.to_s.gsub(/\s/, '') =~ %r{\A(|[0-9a-zA-Z\+=/]{4}+)\Z}).nil?
|
51
|
+
when 'instant'
|
52
|
+
formatted_value = value.respond_to?(:xmlschema) ? value.xmlschema : value.to_s
|
53
|
+
!(formatted_value =~ /\A([0-9]{4}(-(0[1-9]|1[0-2])(-(0[0-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\.[0-9]+)?(Z|(\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))))))\Z/).nil?
|
54
|
+
when 'date'
|
55
|
+
!(value.to_s =~ /\A(-?[0-9]{4}(-(0[1-9]|1[0-2])(-(0[0-9]|[1-2][0-9]|3[0-1]))?)?)\Z/).nil?
|
56
|
+
# NOTE: we don't try to instantiate and verify a Date because ruby does not natively suppport
|
57
|
+
# partial dates, which the FHIR standard allows.
|
58
|
+
when 'datetime'
|
59
|
+
!(value.to_s =~ /\A(-?[0-9]{4}(-(0[1-9]|1[0-2])(-(0[0-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\.[0-9]+)?(Z|(\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?)\Z/).nil?
|
60
|
+
# NOTE: we don't try to instantiate and verify a DateTime because ruby does not natively suppport
|
61
|
+
# partial dates, which the FHIR standard allows.
|
62
|
+
when 'time'
|
63
|
+
!(value.to_s =~ /\A(([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\.[0-9]+)?)\Z/).nil?
|
64
|
+
when 'code'
|
65
|
+
!(value.to_s =~ /\A[^\s]+([\s]?[^\s]+)*\Z/).nil?
|
66
|
+
when 'oid'
|
67
|
+
!(value.to_s =~ /\Aurn:oid:[0-2](\.[1-9]\d*)+\Z/).nil?
|
68
|
+
when 'id'
|
69
|
+
!(value.to_s =~ /\A[A-Za-z0-9\-\.]{1,64}\Z/).nil?
|
70
|
+
when 'xhtml'
|
71
|
+
fragment = Nokogiri::HTML::DocumentFragment.parse(value)
|
72
|
+
value.is_a?(String) && fragment.errors.size.zero?
|
73
|
+
when 'unsignedint'
|
74
|
+
!(value.to_s =~ /\A([0]|([1-9][0-9]*))\Z/).nil?
|
75
|
+
when 'positiveint'
|
76
|
+
!(value.to_s =~ /\A+?[1-9][0-9]*\Z/).nil?
|
77
|
+
else
|
78
|
+
FHIR.logger.warn "Unable to check #{value} for datatype #{datatype}"
|
79
|
+
false
|
80
|
+
end
|
81
|
+
end
|
24
82
|
end
|
@@ -21,6 +21,6 @@ module FHIR
|
|
21
21
|
'xhtml' => {'type'=>'string'}
|
22
22
|
}
|
23
23
|
TYPES = ['Reference', 'Quantity', 'Period', 'Attachment', 'Duration', 'Count', 'Range', 'Annotation', 'Money', 'Identifier', 'Coding', 'Signature', 'SampledData', 'Ratio', 'Distance', 'Age', 'CodeableConcept', 'Extension', 'BackboneElement', 'Narrative', 'Element', 'Meta', 'Address', 'TriggerDefinition', 'Contributor', 'DataRequirement', 'RelatedArtifact', 'ContactDetail', 'HumanName', 'ContactPoint', 'UsageContext', 'Timing', 'ElementDefinition', 'DosageInstruction', 'ParameterDefinition']
|
24
|
-
RESOURCES = ['CodeSystem', 'ValueSet', 'DomainResource', 'Parameters', 'Resource', 'Account', 'ActivityDefinition', 'AllergyIntolerance', 'Appointment', 'AppointmentResponse', 'AuditEvent', 'Basic', 'Binary', 'BodySite', 'Bundle', 'CapabilityStatement', 'CarePlan', 'CareTeam', 'Claim', 'ClaimResponse', 'ClinicalImpression', 'Communication', 'CommunicationRequest', 'CompartmentDefinition', 'Composition', 'ConceptMap', 'Condition', 'Consent', 'Contract', 'Coverage', 'DataElement', 'DetectedIssue', 'Device', 'DeviceComponent', 'DeviceMetric', 'DeviceUseRequest', 'DeviceUseStatement', 'DiagnosticReport', 'DiagnosticRequest', 'DocumentManifest', 'DocumentReference', 'EligibilityRequest', 'EligibilityResponse', 'Encounter', 'Endpoint', 'EnrollmentRequest', 'EnrollmentResponse', 'EpisodeOfCare', 'ExpansionProfile', 'ExplanationOfBenefit', 'FamilyMemberHistory', 'Flag', 'Goal', 'Group', 'GuidanceResponse', 'HealthcareService', 'ImagingManifest', 'ImagingStudy', 'Immunization', 'ImmunizationRecommendation', 'ImplementationGuide', 'Library', 'Linkage', 'List', 'Location', 'Measure', 'MeasureReport', 'Media', 'Medication', 'MedicationAdministration', 'MedicationDispense', 'MedicationRequest', 'MedicationStatement', 'MessageDefinition', 'MessageHeader', 'NamingSystem', 'NutritionRequest', 'Observation', 'OperationDefinition', 'OperationOutcome', 'Organization', 'Patient', 'PaymentNotice', 'PaymentReconciliation', 'Person', 'PlanDefinition', 'Practitioner', 'PractitionerRole', 'Procedure', 'ProcedureRequest', 'ProcessRequest', 'ProcessResponse', 'Provenance', 'Questionnaire', 'QuestionnaireResponse', 'ReferralRequest', 'RelatedPerson', 'RequestGroup', 'ResearchStudy', 'ResearchSubject', 'RiskAssessment', 'Schedule', 'SearchParameter', 'Sequence', 'ServiceDefinition', 'Slot', 'Specimen', 'StructureDefinition', 'StructureMap', 'Subscription', 'Substance', 'SupplyDelivery', 'SupplyRequest', 'Task', 'TestReport', 'TestScript', 'VisionPrescription'
|
24
|
+
RESOURCES = ['CodeSystem', 'ValueSet', 'DomainResource', 'Parameters', 'Resource', 'Account', 'ActivityDefinition', 'AllergyIntolerance', 'Appointment', 'AppointmentResponse', 'AuditEvent', 'Basic', 'Binary', 'BodySite', 'Bundle', 'CapabilityStatement', 'CarePlan', 'CareTeam', 'Claim', 'ClaimResponse', 'ClinicalImpression', 'Communication', 'CommunicationRequest', 'CompartmentDefinition', 'Composition', 'ConceptMap', 'Condition', 'Consent', 'Contract', 'Coverage', 'DataElement', 'DetectedIssue', 'Device', 'DeviceComponent', 'DeviceMetric', 'DeviceUseRequest', 'DeviceUseStatement', 'DiagnosticReport', 'DiagnosticRequest', 'DocumentManifest', 'DocumentReference', 'EligibilityRequest', 'EligibilityResponse', 'Encounter', 'Endpoint', 'EnrollmentRequest', 'EnrollmentResponse', 'EpisodeOfCare', 'ExpansionProfile', 'ExplanationOfBenefit', 'FamilyMemberHistory', 'Flag', 'Goal', 'Group', 'GuidanceResponse', 'HealthcareService', 'ImagingManifest', 'ImagingStudy', 'Immunization', 'ImmunizationRecommendation', 'ImplementationGuide', 'Library', 'Linkage', 'List', 'Location', 'Measure', 'MeasureReport', 'Media', 'Medication', 'MedicationAdministration', 'MedicationDispense', 'MedicationRequest', 'MedicationStatement', 'MessageDefinition', 'MessageHeader', 'NamingSystem', 'NutritionRequest', 'Observation', 'OperationDefinition', 'OperationOutcome', 'Organization', 'Patient', 'PaymentNotice', 'PaymentReconciliation', 'Person', 'PlanDefinition', 'Practitioner', 'PractitionerRole', 'Procedure', 'ProcedureRequest', 'ProcessRequest', 'ProcessResponse', 'Provenance', 'Questionnaire', 'QuestionnaireResponse', 'ReferralRequest', 'RelatedPerson', 'RequestGroup', 'ResearchStudy', 'ResearchSubject', 'RiskAssessment', 'Schedule', 'SearchParameter', 'Sequence', 'ServiceDefinition', 'Slot', 'Specimen', 'StructureDefinition', 'StructureMap', 'Subscription', 'Substance', 'SupplyDelivery', 'SupplyRequest', 'Task', 'TestReport', 'TestScript', 'VisionPrescription']
|
25
25
|
|
26
26
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module FHIR
|
2
|
+
# Extend ElementDefinition for profile validation code
|
3
|
+
class ElementDefinition < FHIR::Model
|
4
|
+
# children is used to hierarchically arrange elements
|
5
|
+
# so profile validation is easier to compute
|
6
|
+
attr_accessor :children
|
7
|
+
attr_accessor :marked_for_keeping
|
8
|
+
|
9
|
+
def add_descendent(element)
|
10
|
+
@children = [] if @children.nil?
|
11
|
+
if @children.last && element.path.start_with?(@children.last.path)
|
12
|
+
if element.path == @children.last.path
|
13
|
+
# slicing
|
14
|
+
@children << element
|
15
|
+
else
|
16
|
+
@children.last.add_descendent(element)
|
17
|
+
end
|
18
|
+
else
|
19
|
+
@children << element
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def keep_children(whitelist = [])
|
24
|
+
@marked_for_keeping = true if whitelist.include?(path)
|
25
|
+
return unless @children
|
26
|
+
@children.each do |child|
|
27
|
+
child.keep_children(whitelist)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def sweep_children
|
32
|
+
return unless @children
|
33
|
+
@children.each(&:sweep_children)
|
34
|
+
@children = @children.keep_if(&:marked_for_keeping)
|
35
|
+
@marked_for_keeping = !@children.empty? || @marked_for_keeping
|
36
|
+
end
|
37
|
+
|
38
|
+
def print_children(spaces = 0)
|
39
|
+
puts "#{' ' * spaces}+#{path}"
|
40
|
+
return nil unless @children
|
41
|
+
@children.each do |child|
|
42
|
+
child.print_children(spaces + 2)
|
43
|
+
end
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -1,381 +1,15 @@
|
|
1
|
+
# Extend StructureDefinition for profile validation code
|
1
2
|
require 'nokogiri'
|
2
3
|
require 'yaml'
|
3
4
|
require 'bcp47'
|
4
5
|
|
5
6
|
module FHIR
|
6
7
|
class StructureDefinition
|
8
|
+
extend FHIR::Deprecate
|
7
9
|
attr_accessor :finding
|
8
10
|
attr_accessor :errors
|
9
11
|
attr_accessor :warnings
|
10
|
-
|
11
|
-
# -------------------------------------------------------------------------
|
12
|
-
# Profile Comparison
|
13
|
-
# -------------------------------------------------------------------------
|
14
|
-
|
15
|
-
# Checks whether or not "another_definition" is compatible with this definition.
|
16
|
-
# If they have conflicting elements, restrictions, bindings, modifying extensions, etc.
|
17
|
-
def is_compatible?(another_definition)
|
18
|
-
@errors = []
|
19
|
-
@warnings = []
|
20
|
-
|
21
|
-
@finding = FHIR::StructureDefinitionFinding.new
|
22
|
-
@finding.resourceType = snapshot.element[0].path
|
23
|
-
@finding.profileIdA = id
|
24
|
-
@finding.profileIdB = another_definition.id if another_definition.respond_to?(:id)
|
25
|
-
|
26
|
-
if !(another_definition.is_a? FHIR::StructureDefinition)
|
27
|
-
@errors << @finding.error('', '', 'Not a StructureDefinition', 'StructureDefinition', another_definition.class.name.to_s)
|
28
|
-
return false
|
29
|
-
elsif another_definition.snapshot.element[0].path != snapshot.element[0].path
|
30
|
-
@errors << @finding.error('', '', 'Incompatible resourceType', @finding.resourceType, another_definition.snapshot.element[0].path.to_s)
|
31
|
-
return false
|
32
|
-
end
|
33
|
-
|
34
|
-
left_elements = Array.new(snapshot.element)
|
35
|
-
right_elements = Array.new(another_definition.snapshot.element)
|
36
|
-
|
37
|
-
left_paths = left_elements.map(&:path)
|
38
|
-
right_paths = right_elements.map(&:path)
|
39
|
-
|
40
|
-
# StructureDefinitions don't always include all base attributes (for example, of a ContactPoint)
|
41
|
-
# if nothing is modified from the base definition, so we have to add them in if they are missing.
|
42
|
-
base_definition = FHIR::Definitions.get_resource_definition(snapshot.element[0].path)
|
43
|
-
base_elements = base_definition.snapshot.element
|
44
|
-
|
45
|
-
left_missing = right_paths - left_paths
|
46
|
-
# left_missing_roots = left_missing.map{|e| e.split('.')[0..-2].join('.') }.uniq
|
47
|
-
add_missing_elements(id, left_missing, left_elements, base_elements)
|
48
|
-
|
49
|
-
right_missing = left_paths - right_paths
|
50
|
-
# right_missing_roots = right_missing.map{|e| e.split('.')[0..-2].join('.') }.uniq
|
51
|
-
add_missing_elements(another_definition.id, right_missing, right_elements, base_elements)
|
52
|
-
|
53
|
-
# update paths
|
54
|
-
left_paths = left_elements.map(&:path)
|
55
|
-
right_paths = right_elements.map(&:path)
|
56
|
-
|
57
|
-
# recalculate the missing attributes
|
58
|
-
left_missing = right_paths - left_paths
|
59
|
-
right_missing = left_paths - right_paths
|
60
|
-
|
61
|
-
# generate warnings for missing fields (ignoring extensions)
|
62
|
-
left_missing.each do |e|
|
63
|
-
next if e.include? 'extension'
|
64
|
-
elem = get_element_by_path(e, right_elements)
|
65
|
-
if !elem.min.nil? && elem.min > 0
|
66
|
-
@errors << @finding.error(e, 'min', 'Missing REQUIRED element', 'Missing', elem.min.to_s)
|
67
|
-
elsif elem.isModifier == true
|
68
|
-
@errors << @finding.error(e, 'isModifier', 'Missing MODIFIER element', 'Missing', elem.isModifier.to_s)
|
69
|
-
else
|
70
|
-
@warnings << @finding.warning(e, '', 'Missing element', 'Missing', 'Defined')
|
71
|
-
end
|
72
|
-
end
|
73
|
-
right_missing.each do |e|
|
74
|
-
next if e.include? 'extension'
|
75
|
-
elem = get_element_by_path(e, left_elements)
|
76
|
-
if !elem.min.nil? && elem.min > 0
|
77
|
-
@errors << @finding.error(e, 'min', 'Missing REQUIRED element', elem.min.to_s, 'Missing')
|
78
|
-
elsif elem.isModifier == true
|
79
|
-
@errors << @finding.error(e, 'isModifier', 'Missing MODIFIER element', elem.isModifier.to_s, 'Missing')
|
80
|
-
else
|
81
|
-
@warnings << @finding.warning(e, '', 'Missing element', 'Defined', 'Missing')
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
left_extensions = []
|
86
|
-
right_extensions = []
|
87
|
-
|
88
|
-
# compare elements, starting with the elements in this definition
|
89
|
-
left_elements.each do |x|
|
90
|
-
if x.path.include? 'extension'
|
91
|
-
# handle extensions separately
|
92
|
-
left_extensions << x
|
93
|
-
else
|
94
|
-
y = get_element_by_path(x.path, right_elements)
|
95
|
-
compare_element_definitions(x, y, another_definition)
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
# now compare elements defined in the other definition, if we haven't already looked at them
|
100
|
-
right_elements.each do |y|
|
101
|
-
if y.path.include? 'extension'
|
102
|
-
# handle extensions separately
|
103
|
-
right_extensions << y
|
104
|
-
elsif left_missing.include? y.path
|
105
|
-
x = get_element_by_path(y.path, left_elements)
|
106
|
-
compare_element_definitions(x, y, another_definition)
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
# finally, compare the extensions.
|
111
|
-
checked_extensions = []
|
112
|
-
left_extensions.each do |x|
|
113
|
-
y = get_extension(x.name, right_extensions)
|
114
|
-
unless y.nil?
|
115
|
-
# both profiles share an extension with the same name
|
116
|
-
checked_extensions << x.name
|
117
|
-
compare_extension_definition(x, y, another_definition)
|
118
|
-
end
|
119
|
-
y = get_extension(x.type[0].profile, right_extensions)
|
120
|
-
if !y.nil? && x.name != y.name
|
121
|
-
# both profiles share the same extension definition but with a different name
|
122
|
-
checked_extensions << x.name
|
123
|
-
checked_extensions << y.name
|
124
|
-
compare_element_definitions(x, y, another_definition)
|
125
|
-
end
|
126
|
-
end
|
127
|
-
right_extensions.each do |y|
|
128
|
-
next if checked_extensions.include?(y.name)
|
129
|
-
x = get_extension(y.name, left_extensions)
|
130
|
-
unless x.nil?
|
131
|
-
# both profiles share an extension with the same name
|
132
|
-
checked_extensions << y.name
|
133
|
-
compare_extension_definition(x, y, another_definition)
|
134
|
-
end
|
135
|
-
x = get_extension(y.type[0].profile, left_extensions)
|
136
|
-
if !x.nil? && x.name != y.name && !checked_extensions.include?(x.name)
|
137
|
-
# both profiles share the same extension definition but with a different name
|
138
|
-
checked_extensions << x.name
|
139
|
-
checked_extensions << y.name
|
140
|
-
compare_element_definitions(x, y, another_definition)
|
141
|
-
end
|
142
|
-
end
|
143
|
-
@errors.flatten!
|
144
|
-
@warnings.flatten!
|
145
|
-
@errors.size.zero?
|
146
|
-
end
|
147
|
-
|
148
|
-
def get_element_by_path(path, elements = snapshot.element)
|
149
|
-
elements.detect { |element| element.path == path }
|
150
|
-
end
|
151
|
-
|
152
|
-
def get_extension(extension, elements = snapshot.element)
|
153
|
-
elements.each do |element|
|
154
|
-
if element.path.include?('extension') || element.type.map(&:code).include?('Extension')
|
155
|
-
return element if element.name == extension || element.type.map(&:profile).include?(extension)
|
156
|
-
end
|
157
|
-
end
|
158
|
-
nil
|
159
|
-
end
|
160
|
-
|
161
|
-
# private
|
162
|
-
# name -- name of the profile we're fixing
|
163
|
-
# missing_paths -- list of paths that we're adding
|
164
|
-
# elements -- list of elements currently defined in the profile
|
165
|
-
# base_elements -- list of elements defined in the base resource the profile extends
|
166
|
-
def add_missing_elements(_name, missing_paths, elements, base_elements)
|
167
|
-
variable_paths = elements.map(&:path).grep(/\[x\]/).map { |e| e[0..-4] }
|
168
|
-
variable_paths << base_elements.map(&:path).grep(/\[x\]/).map { |e| e[0..-4] }
|
169
|
-
variable_paths.flatten!.uniq!
|
170
|
-
|
171
|
-
missing_paths.each do |path|
|
172
|
-
# Skip extensions
|
173
|
-
next if path.include? 'extension'
|
174
|
-
|
175
|
-
# Skip the variable paths that end with "[x]"
|
176
|
-
next if variable_paths.any? { |variable| path.starts_with?(variable) }
|
177
|
-
|
178
|
-
elem = get_element_by_path(path, base_elements)
|
179
|
-
unless elem.nil?
|
180
|
-
# _DEEP_ copy
|
181
|
-
elements << FHIR::ElementDefinition.from_fhir_json(elem.to_fhir_json)
|
182
|
-
next
|
183
|
-
end
|
184
|
-
|
185
|
-
x = path.split('.')
|
186
|
-
root = x.first(x.size - 1).join('.')
|
187
|
-
next unless root.include? '.'
|
188
|
-
# get the root element to fill in the details
|
189
|
-
elem = get_element_by_path(root, elements)
|
190
|
-
# get the data type definition to fill in the details
|
191
|
-
# assume missing elements are from first data type (gross)
|
192
|
-
next if elem.type.nil? || elem.type.empty?
|
193
|
-
type_def = FHIR::Definitions.get_type_definition(elem.type[0].code)
|
194
|
-
next if type_def.nil?
|
195
|
-
type_elements = Array.new(type_def.snapshot.element)
|
196
|
-
# _DEEP_ copy
|
197
|
-
type_elements.map! do |e| # {|e| FHIR::ElementDefinition.from_fhir_json(e.to_fhir_json) }
|
198
|
-
FHIR::ElementDefinition.from_fhir_json(e.to_fhir_json)
|
199
|
-
end
|
200
|
-
# Fix path names
|
201
|
-
type_root = String.new(type_elements[0].path)
|
202
|
-
type_elements.each { |e| e.path.gsub!(type_root, root) }
|
203
|
-
# finally, add the missing element definitions
|
204
|
-
# one by one -- only if they are not already present (i.e. do not override)
|
205
|
-
type_elements.each do |z|
|
206
|
-
y = get_element_by_path(z.path, elements)
|
207
|
-
next unless y.nil?
|
208
|
-
elements << z
|
209
|
-
# else
|
210
|
-
# @warnings << "StructureDefinition #{name} already contains #{z.path}"
|
211
|
-
end
|
212
|
-
elements.uniq!
|
213
|
-
# else
|
214
|
-
# @warnings << "StructureDefinition #{name} missing -- #{path}"
|
215
|
-
end
|
216
|
-
end
|
217
|
-
|
218
|
-
# private
|
219
|
-
def compare_extension_definition(x, y, another_definition)
|
220
|
-
x_profiles = x.type.map(&:profile)
|
221
|
-
y_profiles = y.type.map(&:profile)
|
222
|
-
x_only = x_profiles - y_profiles
|
223
|
-
shared = x_profiles - x_only
|
224
|
-
|
225
|
-
if !shared.nil? && shared.size.zero?
|
226
|
-
# same name, but different profiles
|
227
|
-
# maybe the profiles are the same, just with different URLs...
|
228
|
-
# ... so we have to compare them, if we can.
|
229
|
-
@warnings << @finding.warning("#{x.path} (#{x.name})", 'type.profile', 'Different Profiles', x_profiles.to_s, y_profiles.to_s)
|
230
|
-
x_extension = FHIR::Definitions.get_extension_definition(x.type[0].profile)
|
231
|
-
y_extension = FHIR::Definitions.get_extension_definition(y.type[0].profile)
|
232
|
-
if !x_extension.nil? && !y_extension.nil?
|
233
|
-
x_extension.is_compatible?(y_extension)
|
234
|
-
@errors << x_extension.errors
|
235
|
-
@warnings << x_extension.warnings
|
236
|
-
else
|
237
|
-
@warnings << @finding.warning("#{x.path} (#{x.name})", '', 'Could not find extension definitions to compare.', '', '')
|
238
|
-
end
|
239
|
-
else
|
240
|
-
compare_element_definitions(x, y, another_definition)
|
241
|
-
end
|
242
|
-
end
|
243
|
-
|
244
|
-
# private
|
245
|
-
def compare_element_definitions(x, y, another_definition)
|
246
|
-
return if x.nil? || y.nil? || another_definition.nil?
|
247
|
-
|
248
|
-
# check cardinality
|
249
|
-
x_min = x.min || 0
|
250
|
-
x_max = x.max == '*' ? Float::INFINITY : x.max.to_i
|
251
|
-
y_min = y.min || 0
|
252
|
-
y_max = y.max == '*' ? Float::INFINITY : y.max.to_i
|
253
|
-
|
254
|
-
if x_min.nil? || x.max.nil? || y_min.nil? || y.max.nil?
|
255
|
-
@errors << @finding.error(x.path.to_s, 'min/max', 'Unknown cardinality', "#{x_min}..#{x.max}", "#{y_min}..#{y.max}")
|
256
|
-
elsif (x_min > y_max) || (x_max < y_min)
|
257
|
-
@errors << @finding.error(x.path.to_s, 'min/max', 'Incompatible cardinality', "#{x_min}..#{x.max}", "#{y_min}..#{y.max}")
|
258
|
-
elsif (x_min != y_min) || (x_max != y_max)
|
259
|
-
@warnings << @finding.warning(x.path.to_s, 'min/max', 'Inconsistent cardinality', "#{x_min}..#{x.max}", "#{y_min}..#{y.max}")
|
260
|
-
end
|
261
|
-
|
262
|
-
# check data types
|
263
|
-
x_types = x.type.map(&:code)
|
264
|
-
y_types = y.type.map(&:code)
|
265
|
-
x_only = x_types - y_types
|
266
|
-
y_only = y_types - x_types
|
267
|
-
shared = x_types - x_only
|
268
|
-
|
269
|
-
if !shared.nil? && shared.size.zero? && !x_types.empty? && !y_types.empty? && !x.constraint.empty? && !y.constraint.empty?
|
270
|
-
@errors << @finding.error(x.path.to_s, 'type.code', 'Incompatible data types', x_types.to_s, y_types.to_s)
|
271
|
-
end
|
272
|
-
if !x_only.nil? && !x_only.empty?
|
273
|
-
@warnings << @finding.warning(x.path.to_s, 'type.code', 'Allows additional data types', x_only.to_s, 'not allowed')
|
274
|
-
end
|
275
|
-
if !y_only.nil? && !y_only.empty?
|
276
|
-
@warnings << @finding.warning(x.path.to_s, 'type.code', 'Allows additional data types', 'not allowed', y_only.to_s)
|
277
|
-
end
|
278
|
-
|
279
|
-
# check bindings
|
280
|
-
if x.binding.nil? && !y.binding.nil?
|
281
|
-
val = y.binding.valueSetUri || y.binding.valueSetReference.try(:reference) || y.binding.description
|
282
|
-
@warnings << @finding.warning(x.path.to_s, 'binding', 'Inconsistent binding', '', val)
|
283
|
-
elsif !x.binding.nil? && y.binding.nil?
|
284
|
-
val = x.binding.valueSetUri || x.binding.valueSetReference.try(:reference) || x.binding.description
|
285
|
-
@warnings << @finding.warning(x.path.to_s, 'binding', 'Inconsistent binding', val, '')
|
286
|
-
elsif !x.binding.nil? && !y.binding.nil?
|
287
|
-
x_vs = x.binding.valueSetUri || x.binding.valueSetReference.try(:reference)
|
288
|
-
y_vs = y.binding.valueSetUri || y.binding.valueSetReference.try(:reference)
|
289
|
-
if x_vs != y_vs
|
290
|
-
if x.binding.strength == 'required' || y.binding.strength == 'required'
|
291
|
-
@errors << @finding.error(x.path.to_s, 'binding.strength', 'Incompatible bindings', "#{x.binding.strength} #{x_vs}", "#{y.binding.strength} #{y_vs}")
|
292
|
-
else
|
293
|
-
@warnings << @finding.warning(x.path.to_s, 'binding.strength', 'Inconsistent bindings', "#{x.binding.strength} #{x_vs}", "#{y.binding.strength} #{y_vs}")
|
294
|
-
end
|
295
|
-
end
|
296
|
-
end
|
297
|
-
|
298
|
-
# check default values
|
299
|
-
if x.defaultValue.try(:type) != y.defaultValue.try(:type)
|
300
|
-
@errors << @finding.error(x.path.to_s, 'defaultValue', 'Incompatible default type', x.defaultValue.try(:type).to_s, y.defaultValue.try(:type).to_s)
|
301
|
-
end
|
302
|
-
if x.defaultValue.try(:value) != y.defaultValue.try(:value)
|
303
|
-
@errors << @finding.error(x.path.to_s, 'defaultValue', 'Incompatible default value', x.defaultValue.try(:value).to_s, y.defaultValue.try(:value).to_s)
|
304
|
-
end
|
305
|
-
|
306
|
-
# check meaning when missing
|
307
|
-
if x.meaningWhenMissing != y.meaningWhenMissing
|
308
|
-
@errors << @finding.error(x.path.to_s, 'meaningWhenMissing', 'Inconsistent missing meaning', x.meaningWhenMissing.tr(',', ';').to_s, y.meaningWhenMissing.tr(',', ';').to_s)
|
309
|
-
end
|
310
|
-
|
311
|
-
# check fixed values
|
312
|
-
if x.fixed.try(:type) != y.fixed.try(:type)
|
313
|
-
@errors << @finding.error(x.path.to_s, 'fixed', 'Incompatible fixed type', x.fixed.try(:type).to_s, y.fixed.try(:type).to_s)
|
314
|
-
end
|
315
|
-
if x.fixed != y.fixed
|
316
|
-
xfv = x.fixed.try(:value)
|
317
|
-
xfv = xfv.to_xml.delete(/\n/) if x.fixed.try(:value).methods.include?(:to_xml)
|
318
|
-
yfv = y.fixed.try(:value)
|
319
|
-
yfv = yfv.to_xml.delete(/\n/) if y.fixed.try(:value).methods.include?(:to_xml)
|
320
|
-
@errors << @finding.error(x.path.to_s, 'fixed', 'Incompatible fixed value', xfv.to_s, yfv.to_s)
|
321
|
-
end
|
322
|
-
|
323
|
-
# check min values
|
324
|
-
if x.min.try(:type) != y.min.try(:type)
|
325
|
-
@errors << @finding.error(x.path.to_s, 'min', 'Incompatible min type', x.min.try(:type).to_s, y.min.try(:type).to_s)
|
326
|
-
end
|
327
|
-
if x.min.try(:value) != y.min.try(:value)
|
328
|
-
@errors << @finding.error(x.path.to_s, 'min', 'Incompatible min value', x.min.try(:value).to_s, y.min.try(:value).to_s)
|
329
|
-
end
|
330
|
-
|
331
|
-
# check max values
|
332
|
-
if x.max.try(:type) != y.max.try(:type)
|
333
|
-
@errors << @finding.error(x.path.to_s, 'max', 'Incompatible max type', x.max.try(:type).to_s, y.max.try(:type).to_s)
|
334
|
-
end
|
335
|
-
if x.max.try(:value) != y.max.try(:value)
|
336
|
-
@errors << @finding.error(x.path.to_s, 'max', 'Incompatible max value', x.max.try(:value).to_s, y.max.try(:value).to_s)
|
337
|
-
end
|
338
|
-
|
339
|
-
# check pattern values
|
340
|
-
if x.pattern.try(:type) != y.pattern.try(:type)
|
341
|
-
@errors << @finding.error(x.path.to_s, 'pattern', 'Incompatible pattern type', x.pattern.try(:type).to_s, y.pattern.try(:type).to_s)
|
342
|
-
end
|
343
|
-
if x.pattern.try(:value) != y.pattern.try(:value)
|
344
|
-
@errors << @finding.error(x.path.to_s, 'pattern', 'Incompatible pattern value', x.pattern.try(:value).to_s, y.pattern.try(:value).to_s)
|
345
|
-
end
|
346
|
-
|
347
|
-
# maxLength (for Strings)
|
348
|
-
if x.maxLength != y.maxLength
|
349
|
-
@warnings << @finding.warning(x.path.to_s, 'maxLength', 'Inconsistent maximum length', x.maxLength.to_s, y.maxLength.to_s)
|
350
|
-
end
|
351
|
-
|
352
|
-
# constraints
|
353
|
-
x_constraints = x.constraint.map(&:xpath)
|
354
|
-
y_constraints = y.constraint.map(&:xpath)
|
355
|
-
x_only = x_constraints - y_constraints
|
356
|
-
y_only = y_constraints - x_constraints
|
357
|
-
shared = x_constraints - x_only
|
358
|
-
|
359
|
-
if !shared.nil? && shared.size.zero? && !x.constraint.empty? && !y.constraint.empty?
|
360
|
-
@errors << @finding.error(x.path.to_s, 'constraint.xpath', 'Incompatible constraints', x_constraints.map { |z| z.tr(',', ';') }.join(' && ').to_s, y_constraints.map { |z| z.tr(',', ';') }.join(' && ').to_s)
|
361
|
-
end
|
362
|
-
if !x_only.nil? && !x_only.empty?
|
363
|
-
@errors << @finding.error(x.path.to_s, 'constraint.xpath', 'Additional constraints', x_constraints.map { |z| z.tr(',', ';') }.join(' && ').to_s, '')
|
364
|
-
end
|
365
|
-
if !y_only.nil? && !y_only.empty?
|
366
|
-
@errors << @finding.error(x.path.to_s, 'constraint.xpath', 'Additional constraints', '', y_constraints.map { |z| z.tr(',', ';') }.join(' && ').to_s)
|
367
|
-
end
|
368
|
-
|
369
|
-
# mustSupports
|
370
|
-
if x.mustSupport != y.mustSupport
|
371
|
-
@warnings << @finding.warning(x.path.to_s, 'mustSupport', 'Inconsistent mustSupport', (x.mustSupport || false).to_s, (y.mustSupport || false).to_s)
|
372
|
-
end
|
373
|
-
|
374
|
-
# isModifier
|
375
|
-
if x.isModifier != y.isModifier
|
376
|
-
@errors << @finding.error(x.path.to_s, 'isModifier', 'Incompatible isModifier', (x.isModifier || false).to_s, (y.isModifier || false).to_s)
|
377
|
-
end
|
378
|
-
end
|
12
|
+
attr_accessor :hierarchy
|
379
13
|
|
380
14
|
# -------------------------------------------------------------------------
|
381
15
|
# Profile Validation
|
@@ -388,14 +22,26 @@ module FHIR
|
|
388
22
|
def validate_resource(resource)
|
389
23
|
@errors = []
|
390
24
|
@warnings = []
|
391
|
-
|
392
|
-
|
25
|
+
if resource.is_a?(FHIR::Model)
|
26
|
+
valid_json?(resource.to_json) if resource
|
27
|
+
else
|
28
|
+
@errors << "#{resource.class} is not a resource."
|
29
|
+
end
|
30
|
+
@errors
|
31
|
+
end
|
32
|
+
|
33
|
+
def validates_hash?(hash)
|
34
|
+
@errors = []
|
35
|
+
@warnings = []
|
36
|
+
valid_json?(hash) if hash
|
393
37
|
@errors
|
394
38
|
end
|
395
39
|
|
396
40
|
# Checks whether or not the "json" is valid according to this definition.
|
397
41
|
# json == the raw json for a FHIR resource
|
398
|
-
def
|
42
|
+
def valid_json?(json)
|
43
|
+
build_hierarchy if @hierarchy.nil?
|
44
|
+
|
399
45
|
if json.is_a? String
|
400
46
|
begin
|
401
47
|
json = JSON.parse(json)
|
@@ -405,100 +51,35 @@ module FHIR
|
|
405
51
|
end
|
406
52
|
end
|
407
53
|
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
path = element.path
|
412
|
-
path = path[(base_type.size + 1)..-1] if path.start_with? base_type
|
413
|
-
|
414
|
-
nodes = get_json_nodes(json, path)
|
415
|
-
|
416
|
-
# special filtering on extension urls
|
417
|
-
extension_profile = element.type.find { |t| t.code == 'Extension' && !t.profile.nil? && !t.profile.empty? }
|
418
|
-
if extension_profile
|
419
|
-
nodes.keep_if { |x| extension_profile.profile.include?(x['url']) }
|
420
|
-
end
|
421
|
-
|
422
|
-
# Check the cardinality
|
423
|
-
min = element.min
|
424
|
-
max =
|
425
|
-
if element.max == '*'
|
426
|
-
Float::INFINITY
|
427
|
-
else
|
428
|
-
element.max.to_i
|
429
|
-
end
|
430
|
-
if (nodes.size < min) && (nodes.size > max)
|
431
|
-
@errors << "#{element.path} failed cardinality test (#{min}..#{max}) -- found #{nodes.size}"
|
432
|
-
end
|
54
|
+
@hierarchy.children.each do |element|
|
55
|
+
verify_element(element, json)
|
56
|
+
end
|
433
57
|
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
matching_type = 0
|
438
|
-
|
439
|
-
# the element is valid, if it matches at least one of the datatypes
|
440
|
-
temp_messages = []
|
441
|
-
element.type.each do |type|
|
442
|
-
data_type_code = type.code
|
443
|
-
verified_extension = false
|
444
|
-
if data_type_code == 'Extension' && !type.profile.empty?
|
445
|
-
extension_def = FHIR::Definitions.get_extension_definition(value['url'])
|
446
|
-
if extension_def
|
447
|
-
verified_extension = extension_def.validates_resource?(FHIR::Extension.new(deep_copy(value)))
|
448
|
-
end
|
449
|
-
end
|
450
|
-
if verified_extension || is_data_type?(data_type_code, value)
|
451
|
-
matching_type += 1
|
452
|
-
if data_type_code == 'code' # then check the binding
|
453
|
-
unless element.binding.nil?
|
454
|
-
matching_type += check_binding(element, value)
|
455
|
-
end
|
456
|
-
elsif data_type_code == 'CodeableConcept' && !element.pattern.nil? && element.pattern.type == 'CodeableConcept'
|
457
|
-
# TODO: check that the CodeableConcept matches the defined pattern
|
458
|
-
@warnings << "Ignoring defined patterns on CodeableConcept #{element.path}"
|
459
|
-
elsif data_type_code == 'String' && !element.maxLength.nil? && (value.size > element.maxLength)
|
460
|
-
@errors << "#{element.path} exceed maximum length of #{element.maxLength}: #{value}"
|
461
|
-
end
|
462
|
-
else
|
463
|
-
temp_messages << "#{element.path} is not a valid #{data_type_code}: '#{value}'"
|
464
|
-
end
|
465
|
-
end
|
466
|
-
if matching_type <= 0
|
467
|
-
@errors += temp_messages
|
468
|
-
@errors << "#{element.path} did not match one of the valid data types: #{element.type.map(&:code)}"
|
469
|
-
else
|
470
|
-
@warnings += temp_messages
|
471
|
-
end
|
472
|
-
if !element.fixed.nil? && element.fixed != value
|
473
|
-
@errors << "#{element.path} value of '#{value}' did not match fixed value: #{element.fixed}"
|
474
|
-
end
|
475
|
-
end
|
476
|
-
end
|
58
|
+
@errors.size.zero?
|
59
|
+
end
|
60
|
+
deprecate :is_valid_json?, :valid_json?
|
477
61
|
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
unless element.constraint.empty?
|
486
|
-
element.constraint.each do |constraint|
|
487
|
-
if constraint.expression && !nodes.empty?
|
488
|
-
begin
|
489
|
-
result = FluentPath.evaluate(constraint.expression, json)
|
490
|
-
if !result && constraint.severity == 'error'
|
491
|
-
@errors << "#{element.path}: FluentPath expression evaluates to false for #{name} invariant rule #{constraint.key}: #{constraint.human}"
|
492
|
-
end
|
493
|
-
rescue
|
494
|
-
@warnings << "#{element.path}: unable to evaluate FluentPath expression against JSON for #{name} invariant rule #{constraint.key}: #{constraint.human}"
|
495
|
-
end
|
496
|
-
end
|
497
|
-
end
|
62
|
+
def build_hierarchy
|
63
|
+
@hierarchy = nil
|
64
|
+
snapshot.element.each do |element|
|
65
|
+
if @hierarchy.nil?
|
66
|
+
@hierarchy = element
|
67
|
+
else
|
68
|
+
@hierarchy.add_descendent(element)
|
498
69
|
end
|
499
70
|
end
|
71
|
+
changelist = differential.element.map(&:path)
|
72
|
+
@hierarchy.keep_children(changelist)
|
73
|
+
@hierarchy.sweep_children
|
74
|
+
@hierarchy
|
75
|
+
end
|
500
76
|
|
501
|
-
|
77
|
+
def describe_element(element)
|
78
|
+
if element.path.end_with?('.extension', '.modifierExtension') && element.sliceName
|
79
|
+
"#{element.path} (#{element.sliceName})"
|
80
|
+
else
|
81
|
+
element.path
|
82
|
+
end
|
502
83
|
end
|
503
84
|
|
504
85
|
def get_json_nodes(json, path)
|
@@ -528,21 +109,151 @@ module FHIR
|
|
528
109
|
results
|
529
110
|
end
|
530
111
|
|
531
|
-
def
|
532
|
-
|
112
|
+
def verify_element(element, json)
|
113
|
+
path = element.path
|
114
|
+
path = path[(@hierarchy.path.size + 1)..-1] if path.start_with? @hierarchy.path
|
115
|
+
|
116
|
+
begin
|
117
|
+
data_type_found = element.type.first.code
|
118
|
+
rescue
|
119
|
+
data_type_found = nil
|
120
|
+
end
|
121
|
+
|
122
|
+
# get the JSON nodes associated with this element path
|
123
|
+
if path.end_with?('[x]')
|
124
|
+
nodes = []
|
125
|
+
element.type.each do |type|
|
126
|
+
data_type_found = type.code
|
127
|
+
capcode = type.code.clone
|
128
|
+
capcode[0] = capcode[0].upcase
|
129
|
+
nodes = get_json_nodes(json, path.gsub('[x]', capcode))
|
130
|
+
break unless nodes.empty?
|
131
|
+
end
|
132
|
+
else
|
133
|
+
nodes = get_json_nodes(json, path)
|
134
|
+
end
|
135
|
+
|
136
|
+
# special filtering on extension urls
|
137
|
+
extension_profile = element.type.find { |t| t.code == 'Extension' && !t.profile.nil? }
|
138
|
+
if extension_profile
|
139
|
+
nodes.keep_if { |x| extension_profile.profile == x['url'] }
|
140
|
+
end
|
141
|
+
|
142
|
+
# Check the cardinality
|
143
|
+
min = element.min
|
144
|
+
max = element.max == '*' ? Float::INFINITY : element.max.to_i
|
145
|
+
if (nodes.size < min) || (nodes.size > max)
|
146
|
+
@errors << "#{describe_element(element)} failed cardinality test (#{min}..#{max}) -- found #{nodes.size}"
|
147
|
+
end
|
148
|
+
|
149
|
+
return if nodes.empty?
|
150
|
+
# Check the datatype for each node, only if the element has one declared, and it isn't the root element
|
151
|
+
if !element.type.empty? && element.path != id
|
152
|
+
codeable_concept_pattern = element.pattern && element.pattern.is_a?(FHIR::CodeableConcept)
|
153
|
+
matching_pattern = false
|
154
|
+
nodes.each do |value|
|
155
|
+
matching_type = 0
|
156
|
+
|
157
|
+
# the element is valid, if it matches at least one of the datatypes
|
158
|
+
temp_messages = []
|
159
|
+
verified_extension = false
|
160
|
+
verified_data_type = false
|
161
|
+
if data_type_found == 'Extension' # && !type.profile.nil?
|
162
|
+
verified_extension = true
|
163
|
+
# TODO: should verify extensions
|
164
|
+
# extension_def = FHIR::Definitions.get_extension_definition(value['url'])
|
165
|
+
# if extension_def
|
166
|
+
# verified_extension = extension_def.validates_resource?(FHIR::Extension.new(deep_copy(value)))
|
167
|
+
# end
|
168
|
+
elsif data_type_found
|
169
|
+
temp = @errors
|
170
|
+
@errors = []
|
171
|
+
verified_data_type = data_type?(data_type_found, value)
|
172
|
+
temp_messages << @errors
|
173
|
+
@errors = temp
|
174
|
+
end
|
175
|
+
if data_type_found && (verified_extension || verified_data_type)
|
176
|
+
matching_type += 1
|
177
|
+
if data_type_found == 'code' # then check the binding
|
178
|
+
unless element.binding.nil?
|
179
|
+
matching_type += check_binding(element, value)
|
180
|
+
end
|
181
|
+
elsif data_type_found == 'CodeableConcept' && codeable_concept_pattern
|
182
|
+
vcc = FHIR::CodeableConcept.new(value)
|
183
|
+
pattern = element.pattern.coding
|
184
|
+
pattern.each do |pcoding|
|
185
|
+
vcc.coding.each do |vcoding|
|
186
|
+
matching_pattern = true if vcoding.system == pcoding.system && vcoding.code == pcoding.code
|
187
|
+
end
|
188
|
+
end
|
189
|
+
elsif data_type_found == 'String' && !element.maxLength.nil? && (value.size > element.maxLength)
|
190
|
+
@errors << "#{describe_element(element)} exceed maximum length of #{element.maxLength}: #{value}"
|
191
|
+
end
|
192
|
+
elsif data_type_found
|
193
|
+
temp_messages << "#{describe_element(element)} is not a valid #{data_type_found}: '#{value}'"
|
194
|
+
else
|
195
|
+
# we don't know the data type... so we say "OK"
|
196
|
+
matching_type += 1
|
197
|
+
@warnings >> "Unable to guess data type for #{describe_element(element)}"
|
198
|
+
end
|
199
|
+
|
200
|
+
if matching_type <= 0
|
201
|
+
@errors += temp_messages
|
202
|
+
@errors << "#{describe_element(element)} did not match one of the valid data types: #{element.type.map(&:code)}"
|
203
|
+
else
|
204
|
+
@warnings += temp_messages
|
205
|
+
end
|
206
|
+
if !element.fixed.nil? && element.fixed != value
|
207
|
+
@errors << "#{describe_element(element)} value of '#{value}' did not match fixed value: #{element.fixed}"
|
208
|
+
end
|
209
|
+
end
|
210
|
+
if codeable_concept_pattern && matching_pattern == false
|
211
|
+
@errors << "#{describe_element(element)} CodeableConcept did not match defined pattern: #{element.pattern.to_hash}"
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# Check FluentPath invariants 'constraint.xpath' constraints...
|
216
|
+
# This code is not very robust, and is likely to be throwing *many* exceptions.
|
217
|
+
# This is partially because the FluentPath evaluator is not complete, and partially
|
218
|
+
# because the context of an expression (element.constraint.expression) is not always
|
219
|
+
# consistent with the current context (element.path). For example, sometimes expressions appear to be
|
220
|
+
# written to be evaluated within the element, other times at the resource level, or perhaps
|
221
|
+
# elsewhere. There is no good way to determine "where" you should evaluate the expression.
|
222
|
+
element.constraint.each do |constraint|
|
223
|
+
next unless constraint.expression && !nodes.empty?
|
224
|
+
nodes.each do |node|
|
225
|
+
begin
|
226
|
+
result = FluentPath.evaluate(constraint.expression, node)
|
227
|
+
if !result && constraint.severity == 'error'
|
228
|
+
@errors << "#{describe_element(element)}: FluentPath expression evaluates to false for #{name} invariant rule #{constraint.key}: #{constraint.human}"
|
229
|
+
@errors << node.to_s
|
230
|
+
end
|
231
|
+
rescue
|
232
|
+
@warnings << "#{describe_element(element)}: unable to evaluate FluentPath expression against JSON for #{name} invariant rule #{constraint.key}: #{constraint.human}"
|
233
|
+
@warnings << node.to_s
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
# check children if the element has any
|
239
|
+
return unless element.children
|
240
|
+
element.children.each do |child|
|
241
|
+
verify_element(child, json)
|
242
|
+
end
|
533
243
|
end
|
534
244
|
|
535
245
|
# data_type_code == a FHIR DataType code (see http://hl7.org/fhir/2015May/datatypes.html)
|
536
246
|
# value == the representation of the value
|
537
|
-
def
|
247
|
+
def data_type?(data_type_code, value)
|
538
248
|
# FHIR models covers any base Resources
|
539
249
|
if FHIR::RESOURCES.include?(data_type_code)
|
540
|
-
definition = FHIR::Definitions.
|
250
|
+
definition = FHIR::Definitions.resource_definition(data_type_code)
|
541
251
|
unless definition.nil?
|
542
252
|
ret_val = false
|
543
253
|
begin
|
544
|
-
klass = Module.const_get("FHIR::#{data_type_code}")
|
545
|
-
ret_val = definition.validates_resource?(klass.new(deep_copy(value)))
|
254
|
+
# klass = Module.const_get("FHIR::#{data_type_code}")
|
255
|
+
# ret_val = definition.validates_resource?(klass.new(deep_copy(value)))
|
256
|
+
ret_val = definition.validates_hash?(value)
|
546
257
|
unless ret_val
|
547
258
|
@errors += definition.errors
|
548
259
|
@warnings += definition.warnings
|
@@ -558,60 +269,15 @@ module FHIR
|
|
558
269
|
case data_type_code.downcase
|
559
270
|
when 'domainresource'
|
560
271
|
true # we don't have to verify domain resource, because it will be included in the snapshot
|
561
|
-
when 'boolean'
|
562
|
-
value == true || value == false || value.downcase == 'true' || value.downcase == 'false'
|
563
|
-
when 'code'
|
564
|
-
value.is_a?(String) && value.size >= 1 && value.size == value.rstrip.size
|
565
|
-
when 'string', 'markdown'
|
566
|
-
value.is_a?(String)
|
567
|
-
when 'xhtml'
|
568
|
-
fragment = Nokogiri::HTML::DocumentFragment.parse(value)
|
569
|
-
value.is_a?(String) && fragment.errors.size.zero?
|
570
|
-
when 'base64binary'
|
571
|
-
regex = /[^0-9\+\/\=A-Za-z\r\n ]/
|
572
|
-
value.is_a?(String) && (regex =~ value).nil?
|
573
|
-
when 'id'
|
574
|
-
regex = /[^\d\w\-\.]/
|
575
|
-
# the FHIR spec says IDs have a length limit of 36 characters. But it also says that OIDs
|
576
|
-
# are valid IDs, and ISO OIDs have no length limitations.
|
577
|
-
value.is_a?(String) && (regex =~ value).nil? # && value.size<=36
|
578
|
-
when 'oid'
|
579
|
-
regex = /[^(urn:oid:)[\d\.]]/
|
580
|
-
value.is_a?(String) && (regex =~ value).nil?
|
581
|
-
when 'uri'
|
582
|
-
is_valid_uri = false
|
583
|
-
begin
|
584
|
-
is_valid_uri = !URI.parse(value).nil?
|
585
|
-
rescue
|
586
|
-
is_valid_uri = false
|
587
|
-
end
|
588
|
-
is_valid_uri
|
589
|
-
when 'instant'
|
590
|
-
regex = /\A[0-9]{4}(-(0[1-9]|1[0-2])(-(0[0-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\.[0-9]+)?(Z|(\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))))\Z/
|
591
|
-
value.is_a?(String) && !(regex =~ value).nil?
|
592
|
-
when 'date'
|
593
|
-
regex = /\A[0-9]{4}(-(0[1-9]|1[0-2])(-(0[0-9]|[1-2][0-9]|3[0-1]))?)?\Z/
|
594
|
-
value.is_a?(String) && !(regex =~ value).nil?
|
595
|
-
when 'datetime'
|
596
|
-
regex = /\A[0-9]{4}(-(0[1-9]|1[0-2])(-(0[0-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\.[0-9]+)?(Z|(\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))?)?)?)?\Z/
|
597
|
-
value.is_a?(String) && !(regex =~ value).nil?
|
598
|
-
when 'time'
|
599
|
-
regex = /\A([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\.[0-9]+)?\Z/
|
600
|
-
value.is_a?(String) && !(regex =~ value).nil?
|
601
|
-
when 'integer', 'unsignedint'
|
602
|
-
(!Integer(value).nil? rescue false)
|
603
|
-
when 'positiveint'
|
604
|
-
(!Integer(value).nil? rescue false) && (Integer(value) >= 0)
|
605
|
-
when 'decimal'
|
606
|
-
(!Float(value).nil? rescue false)
|
607
272
|
when 'resource'
|
608
273
|
resource_type = value['resourceType']
|
609
|
-
definition = FHIR::Definitions.
|
274
|
+
definition = FHIR::Definitions.resource_definition(resource_type)
|
610
275
|
if !definition.nil?
|
611
276
|
ret_val = false
|
612
277
|
begin
|
613
|
-
klass = Module.const_get("FHIR::#{resource_type}")
|
614
|
-
ret_val = definition.validates_resource?(klass.new(deep_copy(value)))
|
278
|
+
# klass = Module.const_get("FHIR::#{resource_type}")
|
279
|
+
# ret_val = definition.validates_resource?(klass.new(deep_copy(value)))
|
280
|
+
ret_val = definition.validates_hash?(value)
|
615
281
|
unless ret_val
|
616
282
|
@errors += definition.errors
|
617
283
|
@warnings += definition.warnings
|
@@ -624,17 +290,20 @@ module FHIR
|
|
624
290
|
@errors << "Unable to find base Resource definition: #{resource_type}"
|
625
291
|
false
|
626
292
|
end
|
293
|
+
when *FHIR::PRIMITIVES.keys.map(&:downcase)
|
294
|
+
FHIR.primitive?(datatype: data_type_code, value: value)
|
627
295
|
else
|
628
296
|
# Eliminate endless loop on Element is an Element
|
629
297
|
return true if data_type_code == 'Element' && id == 'Element'
|
630
298
|
|
631
|
-
definition = FHIR::Definitions.
|
632
|
-
definition = FHIR::Definitions.
|
299
|
+
definition = FHIR::Definitions.type_definition(data_type_code)
|
300
|
+
definition = FHIR::Definitions.resource_definition(data_type_code) if definition.nil?
|
633
301
|
if !definition.nil?
|
634
302
|
ret_val = false
|
635
303
|
begin
|
636
|
-
klass = Module.const_get("FHIR::#{data_type_code}")
|
637
|
-
ret_val = definition.validates_resource?(klass.new(deep_copy(value)))
|
304
|
+
# klass = Module.const_get("FHIR::#{data_type_code}")
|
305
|
+
# ret_val = definition.validates_resource?(klass.new(deep_copy(value)))
|
306
|
+
ret_val = definition.validates_hash?(value)
|
638
307
|
unless ret_val
|
639
308
|
@errors += definition.errors
|
640
309
|
@warnings += definition.warnings
|
@@ -649,6 +318,7 @@ module FHIR
|
|
649
318
|
end
|
650
319
|
end
|
651
320
|
end
|
321
|
+
deprecate :is_data_type?, :data_type?
|
652
322
|
|
653
323
|
def check_binding(element, value)
|
654
324
|
vs_uri = element.binding.valueSetUri || element.binding.valueSetReference.reference
|
@@ -658,7 +328,7 @@ module FHIR
|
|
658
328
|
|
659
329
|
if vs_uri == 'http://hl7.org/fhir/ValueSet/content-type' || vs_uri == 'http://www.rfc-editor.org/bcp/bcp13.txt'
|
660
330
|
matches = MIME::Types[value]
|
661
|
-
if (matches.nil? || matches.size.zero?) && !
|
331
|
+
if (matches.nil? || matches.size.zero?) && !some_type_of_xml_or_json?(value)
|
662
332
|
@errors << "#{element.path} has invalid mime-type: '#{value}'"
|
663
333
|
matching_type -= 1 if element.binding.strength == 'required'
|
664
334
|
end
|
@@ -671,7 +341,15 @@ module FHIR
|
|
671
341
|
end
|
672
342
|
elsif valueset.nil?
|
673
343
|
@warnings << "#{element.path} has unknown ValueSet: '#{vs_uri}'"
|
674
|
-
|
344
|
+
if element.binding.strength == 'required'
|
345
|
+
if element.short
|
346
|
+
@warnings << "#{element.path} guessing codes for ValueSet: '#{vs_uri}'"
|
347
|
+
guess_codes = element.short.split(' | ')
|
348
|
+
matching_type -= 1 unless guess_codes.include?(value)
|
349
|
+
else
|
350
|
+
matching_type -= 1
|
351
|
+
end
|
352
|
+
end
|
675
353
|
elsif !valueset.values.flatten.include?(value)
|
676
354
|
message = "#{element.path} has invalid code '#{value}' from #{valueset}"
|
677
355
|
if element.binding.strength == 'required'
|
@@ -685,7 +363,7 @@ module FHIR
|
|
685
363
|
matching_type
|
686
364
|
end
|
687
365
|
|
688
|
-
def
|
366
|
+
def some_type_of_xml_or_json?(code)
|
689
367
|
m = code.downcase
|
690
368
|
return true if m == 'xml' || m == 'json'
|
691
369
|
return true if (m.starts_with?('application/') || m.starts_with?('text/')) && (m.ends_with?('json') || m.ends_with?('xml'))
|
@@ -693,7 +371,8 @@ module FHIR
|
|
693
371
|
return true if m.starts_with?('application/json') || m.starts_with?('text/json')
|
694
372
|
false
|
695
373
|
end
|
374
|
+
deprecate :is_some_type_of_xml_or_json, :some_type_of_xml_or_json?
|
696
375
|
|
697
|
-
private :
|
376
|
+
private :valid_json?, :get_json_nodes, :build_hierarchy, :verify_element, :check_binding
|
698
377
|
end
|
699
378
|
end
|