fhir_models 4.2.1 → 4.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +1 -1
- data/.gitignore +0 -1
- data/.rubocop.yml +5 -1
- data/.rubocop_todo.yml +49 -116
- data/.ruby-version +1 -0
- data/.simplecov +1 -5
- data/.tool-versions +1 -0
- data/Gemfile.lock +29 -23
- data/Guardfile +1 -1
- data/fhir_models.gemspec +2 -1
- data/lib/fhir_models/bootstrap/definitions.rb +16 -3
- data/lib/fhir_models/bootstrap/field.rb +2 -2
- data/lib/fhir_models/bootstrap/generator.rb +9 -7
- data/lib/fhir_models/bootstrap/hashable.rb +23 -17
- data/lib/fhir_models/bootstrap/json.rb +1 -1
- data/lib/fhir_models/bootstrap/model.rb +43 -32
- data/lib/fhir_models/bootstrap/preprocess.rb +25 -17
- data/lib/fhir_models/bootstrap/xml.rb +9 -7
- data/lib/fhir_models/deprecate.rb +1 -0
- data/lib/fhir_models/fhir.rb +7 -6
- data/lib/fhir_models/fhir_ext/element_definition.rb +4 -1
- data/lib/fhir_models/fhir_ext/structure_definition.rb +24 -20
- data/lib/fhir_models/fhir_ext/structure_definition_compare.rb +12 -2
- data/lib/fhir_models/fhir_ext/structure_definition_finding.rb +2 -2
- data/lib/fhir_models/fhirpath/expression.rb +4 -6
- data/lib/fhir_models/fhirpath/parse.rb +10 -7
- data/lib/fhir_models/version.rb +1 -1
- metadata +11 -9
@@ -25,25 +25,29 @@ module FHIR
|
|
25
25
|
# Inspired by active-support `blank` but in our case false isn't blank ...
|
26
26
|
# https://github.com/rails/rails/blob/v5.2.3/activesupport/lib/active_support/core_ext/object/blank.rb
|
27
27
|
blank = ->(obj) { obj.respond_to?(:empty?) ? obj.empty? : obj.nil? }
|
28
|
-
|
28
|
+
case thing
|
29
|
+
when Array
|
29
30
|
return nil if thing.empty?
|
30
|
-
|
31
|
-
thing
|
32
|
-
|
31
|
+
|
32
|
+
thing
|
33
|
+
.map { |i| prune(i) }
|
34
|
+
.reject(&blank)
|
35
|
+
when Hash
|
33
36
|
return {} if thing.empty?
|
37
|
+
|
38
|
+
new_thing = {}
|
34
39
|
thing.each do |key, value|
|
35
|
-
|
36
|
-
end
|
37
|
-
thing.delete_if do |_key, value|
|
38
|
-
blank.call(value)
|
40
|
+
new_thing[key] = prune(value) unless blank.call(value)
|
39
41
|
end
|
42
|
+
new_thing
|
43
|
+
else
|
44
|
+
thing
|
40
45
|
end
|
41
|
-
thing
|
42
46
|
end
|
43
47
|
|
44
|
-
def from_hash(
|
48
|
+
def from_hash(original_hash)
|
45
49
|
# eliminate empty stuff
|
46
|
-
|
50
|
+
pruned_hash = prune(original_hash) unless original_hash.empty?
|
47
51
|
# clear the existing variables
|
48
52
|
self.class::METADATA.each do |key, value|
|
49
53
|
local_name = key
|
@@ -51,15 +55,17 @@ module FHIR
|
|
51
55
|
instance_variable_set("@#{local_name}", nil)
|
52
56
|
end
|
53
57
|
# set the variables to the hash values
|
54
|
-
|
58
|
+
pruned_hash&.each_key do |key|
|
59
|
+
value = original_hash[key]
|
55
60
|
key = key.to_s
|
56
61
|
meta = self.class::METADATA[key]
|
57
62
|
next if meta.nil?
|
63
|
+
|
58
64
|
local_name = key
|
59
65
|
local_name = meta['local_name'] if meta['local_name']
|
60
66
|
begin
|
61
67
|
instance_variable_set("@#{local_name}", value)
|
62
|
-
rescue
|
68
|
+
rescue StandardError
|
63
69
|
# TODO: this appears to be a dead code branch
|
64
70
|
nil
|
65
71
|
end
|
@@ -98,7 +104,7 @@ module FHIR
|
|
98
104
|
if child['resourceType'] && !klass::METADATA['resourceType']
|
99
105
|
klass = begin
|
100
106
|
FHIR.const_get(child['resourceType'])
|
101
|
-
rescue =>
|
107
|
+
rescue StandardError => _e
|
102
108
|
# TODO: this appears to be a dead code branch
|
103
109
|
# TODO: should this log / re-raise the exception if encountered instead of silently swallowing it?
|
104
110
|
FHIR.logger.error("Unable to identify embedded class #{child['resourceType']}\n#{exception.backtrace}")
|
@@ -107,9 +113,9 @@ module FHIR
|
|
107
113
|
end
|
108
114
|
begin
|
109
115
|
obj = klass.new(child)
|
110
|
-
rescue =>
|
116
|
+
rescue StandardError => e
|
111
117
|
# TODO: should this re-raise the exception if encountered instead of silently swallowing it?
|
112
|
-
FHIR.logger.error("Unable to inflate embedded class #{klass}\n#{
|
118
|
+
FHIR.logger.error("Unable to inflate embedded class #{klass}\n#{e.backtrace}")
|
113
119
|
end
|
114
120
|
obj
|
115
121
|
end
|
@@ -121,7 +127,7 @@ module FHIR
|
|
121
127
|
if meta['type'] == 'boolean'
|
122
128
|
rval = value.strip == 'true'
|
123
129
|
elsif FHIR::PRIMITIVES.include?(meta['type'])
|
124
|
-
if
|
130
|
+
if ['decimal', 'integer', 'positiveInt', 'unsignedInt'].include?(meta['type'])
|
125
131
|
rval = BigDecimal(value.to_s)
|
126
132
|
rval = rval.frac.zero? ? rval.to_i : rval.to_f
|
127
133
|
end # primitive is number
|
@@ -17,7 +17,7 @@ module FHIR
|
|
17
17
|
resource_type = hash['resourceType']
|
18
18
|
klass = Module.const_get("FHIR::#{resource_type}")
|
19
19
|
resource = klass.new(hash)
|
20
|
-
rescue => e
|
20
|
+
rescue StandardError => e
|
21
21
|
FHIR.logger.error("Failed to deserialize JSON:\n#{e.backtrace}")
|
22
22
|
FHIR.logger.debug("JSON:\n#{json}")
|
23
23
|
resource = nil
|
@@ -22,40 +22,39 @@ module FHIR
|
|
22
22
|
to_hash.hash
|
23
23
|
end
|
24
24
|
|
25
|
+
def respond_to_missing?(method_name, *)
|
26
|
+
(defined?(self.class::MULTIPLE_TYPES) && self.class::MULTIPLE_TYPES[method_name.to_s]) ||
|
27
|
+
(!@extension.nil? && !@extension.empty? && !find_extension(@extension, method_name).first.nil?) ||
|
28
|
+
(!@modifierExtension.nil? && !@modifierExtension.empty? && !find_extension(@modifierExtension, method_name).first.nil?) ||
|
29
|
+
super
|
30
|
+
end
|
31
|
+
|
25
32
|
# allow two FHIR models to be compared for equality
|
26
33
|
def ==(other)
|
27
34
|
self.class == other.class && to_hash == other.to_hash
|
28
35
|
end
|
29
36
|
alias eql? ==
|
30
37
|
|
31
|
-
def method_missing(
|
32
|
-
if defined?(self.class::MULTIPLE_TYPES) && self.class::MULTIPLE_TYPES[
|
33
|
-
self.class::MULTIPLE_TYPES[
|
38
|
+
def method_missing(method_name, *_args, &_block)
|
39
|
+
if defined?(self.class::MULTIPLE_TYPES) && self.class::MULTIPLE_TYPES[method_name.to_s]
|
40
|
+
self.class::MULTIPLE_TYPES[method_name.to_s].each do |type|
|
34
41
|
type[0] = type[0].upcase
|
35
|
-
value = send("#{
|
42
|
+
value = send("#{method_name}#{type}".to_sym)
|
36
43
|
return value unless value.nil?
|
37
44
|
end
|
38
45
|
return nil
|
39
46
|
elsif !@extension.nil? && !@extension.empty?
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
(method.to_s == name || method.to_s == anchor)
|
44
|
-
end
|
45
|
-
unless ext.first.nil?
|
46
|
-
return ext.first.value.nil? ? ext.first : ext.first.value
|
47
|
+
desired_extension = find_extension(@extension, method_name)
|
48
|
+
unless desired_extension.first.nil?
|
49
|
+
return desired_extension.first.value.nil? ? desired_extension.first : desired_extension.first.value
|
47
50
|
end
|
48
51
|
elsif !@modifierExtension.nil? && !@modifierExtension.empty?
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
(method.to_s == name || method.to_s == anchor)
|
53
|
-
end
|
54
|
-
unless ext.first.nil?
|
55
|
-
return ext.first.value.nil? ? ext.first : ext.first.value
|
52
|
+
desired_extension = find_extension(@modifierExtension, method_name)
|
53
|
+
unless desired_extension.first.nil?
|
54
|
+
return desired_extension.first.value.nil? ? desired_extension.first : desired_extension.first.value
|
56
55
|
end
|
57
56
|
end
|
58
|
-
raise NoMethodError.new("undefined method `#{
|
57
|
+
raise NoMethodError.new("undefined method `#{method_name}' for #{self.class.name}", method_name)
|
59
58
|
end
|
60
59
|
|
61
60
|
def to_reference
|
@@ -141,14 +140,14 @@ module FHIR
|
|
141
140
|
end # metadata.each
|
142
141
|
# check multiple types
|
143
142
|
multiple_types = begin
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
143
|
+
self.class::MULTIPLE_TYPES
|
144
|
+
rescue StandardError
|
145
|
+
{}
|
146
|
+
end
|
148
147
|
multiple_types.each do |prefix, suffixes|
|
149
148
|
present = []
|
150
149
|
suffixes.each do |suffix|
|
151
|
-
typename = "#{prefix}#{suffix[0].upcase}#{suffix[1
|
150
|
+
typename = "#{prefix}#{suffix[0].upcase}#{suffix[1..]}"
|
152
151
|
# check which multiple data types are actually present, not just errors
|
153
152
|
# actually, this might be allowed depending on cardinality
|
154
153
|
value = instance_variable_get("@#{typename}")
|
@@ -157,8 +156,9 @@ module FHIR
|
|
157
156
|
errors[prefix] = ["#{prefix}[x]: more than one type present."] if present.length > 1
|
158
157
|
# remove errors for suffixes that are not present
|
159
158
|
next unless present.length == 1
|
159
|
+
|
160
160
|
suffixes.each do |suffix|
|
161
|
-
typename = "#{prefix}#{suffix[0].upcase}#{suffix[1
|
161
|
+
typename = "#{prefix}#{suffix[0].upcase}#{suffix[1..]}"
|
162
162
|
errors.delete(typename) unless present.include?(typename)
|
163
163
|
end
|
164
164
|
end
|
@@ -221,10 +221,12 @@ module FHIR
|
|
221
221
|
# check binding
|
222
222
|
next unless meta['binding']
|
223
223
|
next unless meta['binding']['strength'] == 'required'
|
224
|
+
|
224
225
|
the_codes = [v]
|
225
|
-
|
226
|
+
case meta['type']
|
227
|
+
when 'Coding'
|
226
228
|
the_codes = [v.code]
|
227
|
-
|
229
|
+
when 'CodeableConcept'
|
228
230
|
the_codes = v.coding.map(&:code).compact
|
229
231
|
end
|
230
232
|
has_valid_code = false
|
@@ -247,6 +249,7 @@ module FHIR
|
|
247
249
|
def validate_reference_type(ref, meta, contained_here, errors)
|
248
250
|
return unless ref.reference && meta['type_profiles']
|
249
251
|
return if ref.reference.start_with?('urn:uuid:', 'urn:oid:')
|
252
|
+
|
250
253
|
matches_one_profile = false
|
251
254
|
meta['type_profiles'].each do |p|
|
252
255
|
basetype = p.split('/').last
|
@@ -258,7 +261,7 @@ module FHIR
|
|
258
261
|
matches_one_profile = true if meta['type_profiles'].include?('http://hl7.org/fhir/StructureDefinition/Resource')
|
259
262
|
if !matches_one_profile && ref.reference.start_with?('#')
|
260
263
|
# we need to look at the local contained resources
|
261
|
-
r = contained_here.find { |x| x.id == ref.reference[1
|
264
|
+
r = contained_here.find { |x| x.id == ref.reference[1..] }
|
262
265
|
if !r.nil?
|
263
266
|
meta['type_profiles'].each do |p|
|
264
267
|
p = p.split('/').last
|
@@ -280,15 +283,15 @@ module FHIR
|
|
280
283
|
def check_binding_uri(uri, value)
|
281
284
|
valid = false
|
282
285
|
# Strip off the |4.0.0 or |4.0.1 or |2014-03-26 or similar from the ends of URLs
|
283
|
-
uri&.gsub!(/\|[A-Za-z0-9
|
286
|
+
uri&.gsub!(/\|[A-Za-z0-9.\-]*/, '')
|
284
287
|
valueset = FHIR::Definitions.get_codes(uri)
|
285
288
|
|
286
|
-
if
|
289
|
+
if ['http://hl7.org/fhir/ValueSet/mimetypes', 'http://www.rfc-editor.org/bcp/bcp13.txt'].include?(uri)
|
287
290
|
matches = MIME::Types[value]
|
288
291
|
json_or_xml = value.downcase.include?('xml') || value.downcase.include?('json')
|
289
292
|
known_weird = ['text/cql', 'application/cql+text', 'application/hl7-v2'].include?(value)
|
290
293
|
valid = json_or_xml || known_weird || (!matches.nil? && !matches.empty?)
|
291
|
-
elsif
|
294
|
+
elsif ['http://hl7.org/fhir/ValueSet/languages', 'http://tools.ietf.org/html/bcp47'].include?(uri)
|
292
295
|
has_region = !(value =~ /-/).nil?
|
293
296
|
valid = !BCP47::Language.identify(value.downcase).nil? && (!has_region || !BCP47::Region.identify(value.upcase).nil?)
|
294
297
|
elsif valueset.nil?
|
@@ -321,6 +324,14 @@ module FHIR
|
|
321
324
|
self
|
322
325
|
end
|
323
326
|
|
324
|
-
|
327
|
+
def find_extension(extension_source, method_name)
|
328
|
+
extension_source.select do |extension|
|
329
|
+
name = extension.url.tr('-', '_').split('/').last
|
330
|
+
anchor = name.split('#').last
|
331
|
+
(method_name.to_s == name || method_name.to_s == anchor)
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
private :validate_reference_type, :check_binding_uri, :validate_field, :find_extension
|
325
336
|
end
|
326
337
|
end
|
@@ -11,16 +11,17 @@ module FHIR
|
|
11
11
|
|
12
12
|
# Remove entries that do not interest us: CompartmentDefinitions, OperationDefinitions, Conformance statements
|
13
13
|
hash['entry'].select! do |entry|
|
14
|
-
|
14
|
+
['StructureDefinition', 'ValueSet', 'CodeSystem', 'SearchParameter'].include? entry['resource']['resourceType']
|
15
15
|
end
|
16
16
|
|
17
17
|
# Remove unnecessary elements from the hash
|
18
18
|
hash['entry'].each do |entry|
|
19
19
|
next unless entry['resource']
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
20
|
+
|
21
|
+
pre_process_structuredefinition(entry['resource']) if entry['resource']['resourceType'] == 'StructureDefinition'
|
22
|
+
pre_process_valueset(entry['resource']) if entry['resource']['resourceType'] == 'ValueSet'
|
23
|
+
pre_process_codesystem(entry['resource']) if entry['resource']['resourceType'] == 'CodeSystem'
|
24
|
+
pre_process_searchparam(entry['resource']) if entry['resource']['resourceType'] == 'SearchParameter'
|
24
25
|
remove_fhir_comments(entry['resource'])
|
25
26
|
end
|
26
27
|
|
@@ -34,26 +35,30 @@ module FHIR
|
|
34
35
|
|
35
36
|
def self.pre_process_structuredefinition(hash)
|
36
37
|
# Remove large HTML narratives and unused content
|
37
|
-
|
38
|
+
['text', 'publisher', 'contact', 'description', 'requirements', 'mapping'].each { |key| hash.delete(key) }
|
38
39
|
|
39
40
|
# Remove unused descriptions within the snapshot and differential elements
|
40
|
-
|
41
|
+
['snapshot', 'differential'].each do |key|
|
41
42
|
next unless hash[key]
|
43
|
+
|
42
44
|
hash[key]['element'].each do |element|
|
43
|
-
|
45
|
+
['short', 'definition', 'comments', 'requirements', 'alias', 'mapping'].each { |subkey| element.delete(subkey) }
|
44
46
|
end
|
45
47
|
end
|
46
48
|
end
|
47
49
|
|
48
50
|
def self.pre_process_valueset(hash)
|
49
51
|
# Remove large HTML narratives and unused content
|
50
|
-
|
52
|
+
['meta', 'text', 'publisher', 'contact', 'description', 'requirements'].each { |key| hash.delete(key) }
|
51
53
|
|
52
54
|
return unless hash['compose']
|
53
|
-
|
55
|
+
|
56
|
+
['include', 'exclude'].each do |key|
|
54
57
|
next unless hash['compose'][key]
|
58
|
+
|
55
59
|
hash['compose'][key].each do |element|
|
56
60
|
next unless element['concept']
|
61
|
+
|
57
62
|
element['concept'].each do |concept|
|
58
63
|
concept.delete('designation')
|
59
64
|
end
|
@@ -63,16 +68,18 @@ module FHIR
|
|
63
68
|
|
64
69
|
def self.pre_process_codesystem(hash)
|
65
70
|
# Remove large HTML narratives and unused content
|
66
|
-
|
71
|
+
['meta', 'text', 'publisher', 'contact', 'description', 'requirements'].each { |key| hash.delete(key) }
|
67
72
|
return unless hash['concept']
|
73
|
+
|
68
74
|
hash['concept'].each do |concept|
|
69
75
|
pre_process_codesystem_concept(concept)
|
70
76
|
end
|
71
77
|
end
|
72
78
|
|
73
79
|
def self.pre_process_codesystem_concept(hash)
|
74
|
-
|
80
|
+
['extension', 'definition', 'designation'].each { |key| hash.delete(key) }
|
75
81
|
return unless hash['concept']
|
82
|
+
|
76
83
|
hash['concept'].each do |concept|
|
77
84
|
pre_process_codesystem_concept(concept)
|
78
85
|
end
|
@@ -80,15 +87,16 @@ module FHIR
|
|
80
87
|
|
81
88
|
def self.pre_process_searchparam(hash)
|
82
89
|
# Remove large HTML narratives and unused content
|
83
|
-
|
90
|
+
['id', 'url', 'name', 'date', 'publisher', 'contact', 'description', 'xpathUsage'].each { |key| hash.delete(key) }
|
84
91
|
end
|
85
92
|
|
86
93
|
def self.remove_fhir_comments(hash)
|
87
94
|
hash.delete('fhir_comments')
|
88
95
|
hash.each do |_key, value|
|
89
|
-
|
96
|
+
case value
|
97
|
+
when Hash
|
90
98
|
remove_fhir_comments(value)
|
91
|
-
|
99
|
+
when Array
|
92
100
|
value.each do |v|
|
93
101
|
remove_fhir_comments(v) if v.is_a?(Hash)
|
94
102
|
end
|
@@ -126,7 +134,7 @@ module FHIR
|
|
126
134
|
hash = JSON.parse(json)
|
127
135
|
|
128
136
|
# Remove narratives
|
129
|
-
|
137
|
+
['text'].each { |key| hash.delete(key) }
|
130
138
|
|
131
139
|
# Output the post processed file
|
132
140
|
f = File.open(filename, 'w:UTF-8')
|
@@ -156,7 +164,7 @@ module FHIR
|
|
156
164
|
# Remove the weird parantheses on xml example filenames
|
157
165
|
# we do this so they match the names of the json examples
|
158
166
|
if filename.include?('(') && filename.include?(')')
|
159
|
-
rename = filename.gsub(/\([A-Za-z0-9
|
167
|
+
rename = filename.gsub(/\([A-Za-z0-9\-.]*\)/, '')
|
160
168
|
File.rename(filename, rename)
|
161
169
|
filename = rename
|
162
170
|
end
|
@@ -24,7 +24,7 @@ module FHIR
|
|
24
24
|
# if hash contains resourceType
|
25
25
|
# create a child node with the name==resourceType
|
26
26
|
# fill that, and place the child under the above `node`
|
27
|
-
if hash['resourceType']
|
27
|
+
if hash['resourceType'].is_a?(String) && name != 'instance'
|
28
28
|
child_name = hash['resourceType']
|
29
29
|
hash.delete('resourceType')
|
30
30
|
child = hash_to_xml_node(child_name, hash, doc)
|
@@ -33,11 +33,13 @@ module FHIR
|
|
33
33
|
end
|
34
34
|
|
35
35
|
hash.each do |key, value|
|
36
|
-
next if
|
36
|
+
next if ['extension', 'modifierExtension'].include?(name) && key == 'url'
|
37
37
|
next if key == 'id' && !FHIR::RESOURCES.include?(name)
|
38
|
-
|
38
|
+
|
39
|
+
case value
|
40
|
+
when Hash
|
39
41
|
node.add_child(hash_to_xml_node(key, value, doc))
|
40
|
-
|
42
|
+
when Array
|
41
43
|
value.each do |v|
|
42
44
|
if v.is_a?(Hash)
|
43
45
|
node.add_child(hash_to_xml_node(key, v, doc))
|
@@ -62,7 +64,7 @@ module FHIR
|
|
62
64
|
node.add_child(child)
|
63
65
|
end
|
64
66
|
end
|
65
|
-
node.set_attribute('url', hash['url']) if
|
67
|
+
node.set_attribute('url', hash['url']) if ['extension', 'modifierExtension'].include?(name)
|
66
68
|
node.set_attribute('id', hash['id']) if hash['id'] && !FHIR::RESOURCES.include?(name)
|
67
69
|
node
|
68
70
|
end
|
@@ -78,7 +80,7 @@ module FHIR
|
|
78
80
|
resource_type = doc.root.name
|
79
81
|
klass = Module.const_get("FHIR::#{resource_type}")
|
80
82
|
resource = klass.new(hash)
|
81
|
-
rescue => e
|
83
|
+
rescue StandardError => e
|
82
84
|
FHIR.logger.error("Failed to deserialize XML:\n#{e.backtrace}")
|
83
85
|
FHIR.logger.debug("XML:\n#{xml}")
|
84
86
|
resource = nil
|
@@ -108,7 +110,7 @@ module FHIR
|
|
108
110
|
end
|
109
111
|
end
|
110
112
|
end
|
111
|
-
hash['url'] = node.get_attribute('url') if
|
113
|
+
hash['url'] = node.get_attribute('url') if ['extension', 'modifierExtension'].include?(node.name)
|
112
114
|
hash['id'] = node.get_attribute('id') if node.get_attribute('id') # Testscript fixture ids (applies to any BackboneElement)
|
113
115
|
hash['resourceType'] = node.name if FHIR::RESOURCES.include?(node.name)
|
114
116
|
|
@@ -10,6 +10,7 @@ module FHIR
|
|
10
10
|
end
|
11
11
|
end
|
12
12
|
return unless methods.include? new_method
|
13
|
+
|
13
14
|
(class << self; self; end).instance_eval do
|
14
15
|
define_method(old_method) do |*args, &block|
|
15
16
|
message = "DEPRECATED: `#{old_method}` has been deprecated. Use `#{new_method}` instead. Called from #{caller.first}"
|
data/lib/fhir_models/fhir.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'nokogiri'
|
2
2
|
require 'logger'
|
3
|
+
require 'uri'
|
3
4
|
|
4
5
|
module FHIR
|
5
6
|
def self.logger
|
@@ -11,7 +12,7 @@ module FHIR
|
|
11
12
|
end
|
12
13
|
|
13
14
|
def self.default_logger
|
14
|
-
@default_logger ||= Logger.new(ENV['FHIR_LOGGER'] ||
|
15
|
+
@default_logger ||= Logger.new(ENV['FHIR_LOGGER'] || $stdout)
|
15
16
|
end
|
16
17
|
|
17
18
|
def self.from_contents(contents)
|
@@ -38,7 +39,7 @@ module FHIR
|
|
38
39
|
when 'uri'
|
39
40
|
begin
|
40
41
|
!URI.parse(value).nil?
|
41
|
-
rescue
|
42
|
+
rescue StandardError
|
42
43
|
false
|
43
44
|
end
|
44
45
|
when 'base64binary'
|
@@ -47,7 +48,7 @@ module FHIR
|
|
47
48
|
# whitespace is not significant so we strip it out before doing the regex so that we can be sure that
|
48
49
|
# the number of characters is a multiple of 4.
|
49
50
|
# https://tools.ietf.org/html/rfc4648
|
50
|
-
!(value.to_s.gsub(/\s/, '') =~ %r{\A(|[0-9a-zA-Z
|
51
|
+
!(value.to_s.gsub(/\s/, '') =~ %r{\A(|[0-9a-zA-Z+=/]{4}+)\Z}).nil?
|
51
52
|
when 'instant'
|
52
53
|
formatted_value = value.respond_to?(:xmlschema) ? value.xmlschema : value.to_s
|
53
54
|
!(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?
|
@@ -62,16 +63,16 @@ module FHIR
|
|
62
63
|
when 'time'
|
63
64
|
!(value.to_s =~ /\A(([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\.[0-9]+)?)\Z/).nil?
|
64
65
|
when 'code'
|
65
|
-
!(value.to_s =~ /\A[^\s]+(
|
66
|
+
!(value.to_s =~ /\A[^\s]+(\s?[^\s]+)*\Z/).nil?
|
66
67
|
when 'oid'
|
67
68
|
!(value.to_s =~ /\Aurn:oid:[0-2](\.[1-9]\d*)+\Z/).nil?
|
68
69
|
when 'id'
|
69
|
-
!(value.to_s =~ /\A[A-Za-z0-9
|
70
|
+
!(value.to_s =~ /\A[A-Za-z0-9\-.]{1,64}\Z/).nil?
|
70
71
|
when 'xhtml'
|
71
72
|
fragment = Nokogiri::HTML::DocumentFragment.parse(value)
|
72
73
|
value.is_a?(String) && fragment.errors.size.zero?
|
73
74
|
when 'unsignedint'
|
74
|
-
!(value.to_s =~ /\A(
|
75
|
+
!(value.to_s =~ /\A(0|([1-9][0-9]*))\Z/).nil?
|
75
76
|
when 'positiveint'
|
76
77
|
!(value.to_s =~ /\A+?[1-9][0-9]*\Z/).nil?
|
77
78
|
else
|
@@ -25,6 +25,7 @@ module FHIR
|
|
25
25
|
def keep_children(whitelist = [])
|
26
26
|
@marked_for_keeping = true if whitelist.include?(path)
|
27
27
|
return unless @children
|
28
|
+
|
28
29
|
@children.each do |child|
|
29
30
|
child.keep_children(whitelist)
|
30
31
|
end
|
@@ -32,14 +33,16 @@ module FHIR
|
|
32
33
|
|
33
34
|
def sweep_children
|
34
35
|
return unless @children
|
36
|
+
|
35
37
|
@children.each(&:sweep_children)
|
36
|
-
@children
|
38
|
+
@children.keep_if(&:marked_for_keeping)
|
37
39
|
@marked_for_keeping = !@children.empty? || @marked_for_keeping
|
38
40
|
end
|
39
41
|
|
40
42
|
def print_children(spaces = 0)
|
41
43
|
puts "#{' ' * spaces}+#{local_name || path}"
|
42
44
|
return nil unless @children
|
45
|
+
|
43
46
|
@children.each do |child|
|
44
47
|
child.print_children(spaces + 2)
|
45
48
|
end
|
@@ -59,7 +59,7 @@ module FHIR
|
|
59
59
|
if json.is_a? String
|
60
60
|
begin
|
61
61
|
json = JSON.parse(json)
|
62
|
-
rescue => e
|
62
|
+
rescue StandardError => e
|
63
63
|
@errors << "Failed to parse JSON: #{e.message} %n #{h} %n #{e.backtrace.join("\n")}"
|
64
64
|
return false
|
65
65
|
end
|
@@ -99,13 +99,15 @@ module FHIR
|
|
99
99
|
def get_json_nodes(json, path)
|
100
100
|
results = []
|
101
101
|
return [json] if path.nil?
|
102
|
+
|
102
103
|
steps = path.split('.')
|
103
104
|
steps.each.with_index do |step, index|
|
104
|
-
|
105
|
+
case json
|
106
|
+
when Hash
|
105
107
|
json = json[step]
|
106
|
-
|
108
|
+
when Array
|
107
109
|
json.each do |e|
|
108
|
-
results << get_json_nodes(e, steps[index
|
110
|
+
results << get_json_nodes(e, steps[index..].join('.'))
|
109
111
|
end
|
110
112
|
return results.flatten!
|
111
113
|
else
|
@@ -125,7 +127,7 @@ module FHIR
|
|
125
127
|
|
126
128
|
def verify_element(element, json)
|
127
129
|
path = element.local_name || element.path
|
128
|
-
path = path[(@hierarchy.path.size + 1)
|
130
|
+
path = path[(@hierarchy.path.size + 1)..] if path.start_with? @hierarchy.path
|
129
131
|
|
130
132
|
if element.type && !element.type.empty?
|
131
133
|
data_type_found = element.type.first.code
|
@@ -157,10 +159,11 @@ module FHIR
|
|
157
159
|
verify_cardinality(element, nodes)
|
158
160
|
|
159
161
|
return if nodes.empty?
|
162
|
+
|
160
163
|
# Check the datatype for each node, only if the element has one declared, and it isn't the root element
|
161
164
|
if !element.type.empty? && element.path != id
|
162
165
|
# element.type not being empty implies data_type_found != nil, for valid profiles
|
163
|
-
codeable_concept_pattern = element.pattern
|
166
|
+
codeable_concept_pattern = element.pattern&.is_a?(FHIR::CodeableConcept)
|
164
167
|
matching_pattern = false
|
165
168
|
nodes.each do |value|
|
166
169
|
matching_type = 0
|
@@ -197,7 +200,7 @@ module FHIR
|
|
197
200
|
matching_pattern = true if vcoding.system == pcoding.system && vcoding.code == pcoding.code
|
198
201
|
end
|
199
202
|
end
|
200
|
-
elsif
|
203
|
+
elsif ['CodeableConcept', 'Coding', 'Quantity'].include? data_type_found
|
201
204
|
required_strength = element&.binding&.strength == 'required'
|
202
205
|
binding_issues = required_strength ? @errors : @warnings
|
203
206
|
|
@@ -275,20 +278,20 @@ module FHIR
|
|
275
278
|
# elsewhere. There is no good way to determine "where" you should evaluate the expression.
|
276
279
|
element.constraint.each do |constraint|
|
277
280
|
next unless constraint.expression && !nodes.empty?
|
281
|
+
|
278
282
|
nodes.each do |node|
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
@errors << "#{describe_element(element)}: FHIRPath expression evaluates to false for #{name} (containing: #{node}) invariant rule #{constraint.key}: #{constraint.human}"
|
283
|
-
end
|
284
|
-
rescue
|
285
|
-
@warnings << "#{describe_element(element)}: unable to evaluate FHIRPath expression against JSON for #{name} (containing: #{node}) invariant rule #{constraint.key}: #{constraint.human}"
|
283
|
+
result = FHIRPath.evaluate(constraint.expression, node)
|
284
|
+
if !result && constraint.severity == 'error'
|
285
|
+
@errors << "#{describe_element(element)}: FHIRPath expression evaluates to false for #{name} (containing: #{node}) invariant rule #{constraint.key}: #{constraint.human}"
|
286
286
|
end
|
287
|
+
rescue StandardError
|
288
|
+
@warnings << "#{describe_element(element)}: unable to evaluate FHIRPath expression against JSON for #{name} (containing: #{node}) invariant rule #{constraint.key}: #{constraint.human}"
|
287
289
|
end
|
288
290
|
end
|
289
291
|
|
290
292
|
# check children if the element has any
|
291
293
|
return unless element.children
|
294
|
+
|
292
295
|
nodes.each do |node|
|
293
296
|
element.children.each do |child|
|
294
297
|
verify_element(child, node)
|
@@ -323,7 +326,7 @@ module FHIR
|
|
323
326
|
@errors += definition.errors
|
324
327
|
@warnings += definition.warnings
|
325
328
|
end
|
326
|
-
rescue
|
329
|
+
rescue StandardError
|
327
330
|
@errors << "Unable to verify #{data_type_code} as a FHIR Resource."
|
328
331
|
end
|
329
332
|
return ret_val
|
@@ -347,7 +350,7 @@ module FHIR
|
|
347
350
|
@errors += definition.errors
|
348
351
|
@warnings += definition.warnings
|
349
352
|
end
|
350
|
-
rescue
|
353
|
+
rescue StandardError
|
351
354
|
@errors << "Unable to verify #{resource_type} as a FHIR Resource."
|
352
355
|
end
|
353
356
|
ret_val
|
@@ -373,7 +376,7 @@ module FHIR
|
|
373
376
|
@errors += definition.errors
|
374
377
|
@warnings += definition.warnings
|
375
378
|
end
|
376
|
-
rescue
|
379
|
+
rescue StandardError
|
377
380
|
@errors << "Unable to verify #{data_type_code} as a FHIR type."
|
378
381
|
end
|
379
382
|
ret_val
|
@@ -395,14 +398,14 @@ module FHIR
|
|
395
398
|
|
396
399
|
matching_type = 0
|
397
400
|
|
398
|
-
if
|
401
|
+
if ['http://hl7.org/fhir/ValueSet/mimetypes', 'http://www.rfc-editor.org/bcp/bcp13.txt'].include?(vs_uri)
|
399
402
|
matches = MIME::Types[value]
|
400
403
|
known_weird = ['text/cql', 'application/cql+text', 'application/hl7-v2'].include?(value)
|
401
404
|
if (matches.nil? || matches.size.zero? || known_weird) && !some_type_of_xml_or_json?(value)
|
402
405
|
@errors << "#{element.path} has invalid mime-type: '#{value}'"
|
403
406
|
matching_type -= 1 if element.binding.strength == 'required'
|
404
407
|
end
|
405
|
-
elsif
|
408
|
+
elsif ['http://hl7.org/fhir/ValueSet/languages', 'http://tools.ietf.org/html/bcp47'].include?(vs_uri)
|
406
409
|
has_region = !(value =~ /-/).nil?
|
407
410
|
valid = !BCP47::Language.identify(value.downcase).nil? && (!has_region || !BCP47::Region.identify(value.upcase).nil?)
|
408
411
|
unless valid
|
@@ -435,9 +438,10 @@ module FHIR
|
|
435
438
|
|
436
439
|
def some_type_of_xml_or_json?(code)
|
437
440
|
m = code.downcase
|
438
|
-
return true if
|
441
|
+
return true if ['xml', 'json'].include?(m)
|
439
442
|
return true if m.start_with?('application/', 'text/') && m.end_with?('json', 'xml')
|
440
443
|
return true if m.start_with?('application/xml', 'text/xml', 'application/json', 'text/json')
|
444
|
+
|
441
445
|
false
|
442
446
|
end
|
443
447
|
deprecate :is_some_type_of_xml_or_json, :some_type_of_xml_or_json?
|