fhir_models 1.8.2 → 1.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|