openehr 2.0.2 → 2.3.0
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/README.rdoc +19 -6
- data/lib/openehr/am/archetype/constraint_model.rb +1 -1
- data/lib/openehr/aql/engine/contains_resolver.rb +98 -7
- data/lib/openehr/aql/engine/dataset.rb +3 -1
- data/lib/openehr/aql/engine/predicate_evaluator.rb +70 -3
- data/lib/openehr/aql/engine.rb +45 -12
- data/lib/openehr/assumed_library_types.rb +20 -17
- data/lib/openehr/parser/adl_parser.rb +0 -4
- data/lib/openehr/parser/opt_parser.rb +12 -429
- data/lib/openehr/parser/xml_archetype_parser.rb +255 -0
- data/lib/openehr/parser/xml_constraint_parsing.rb +264 -0
- data/lib/openehr/parser/xml_domain_type_parsing.rb +128 -0
- data/lib/openehr/parser/xml_primitive_parsing.rb +145 -0
- data/lib/openehr/parser.rb +1 -0
- data/lib/openehr/rm/common/generic.rb +6 -9
- data/lib/openehr/rm/common/resource.rb +0 -1
- data/lib/openehr/rm/composition/content/entry.rb +4 -1
- data/lib/openehr/rm/data_structures/item_structure/representation.rb +12 -2
- data/lib/openehr/rm/data_structures/item_structure.rb +0 -1
- data/lib/openehr/rm/data_types/basic.rb +11 -21
- data/lib/openehr/rm/data_types/encapsulated.rb +18 -2
- data/lib/openehr/rm/data_types/quantity/date_time.rb +203 -28
- data/lib/openehr/rm/data_types/quantity.rb +66 -10
- data/lib/openehr/rm/data_types/time_specification.rb +101 -27
- data/lib/openehr/rm/data_types/uri.rb +0 -8
- data/lib/openehr/rm/ehr.rb +6 -6
- data/lib/openehr/rm/factory.rb +22 -0
- data/lib/openehr/rm/security.rb +3 -1
- data/lib/openehr/rm/type_name.rb +0 -1
- data/lib/openehr/serializer/adl_serializer.rb +239 -84
- data/lib/openehr/serializer/base.rb +2 -0
- data/lib/openehr/serializer/rm_json_serializer.rb +0 -1
- data/lib/openehr/serializer/xml_serializer.rb +351 -103
- data/lib/openehr/version.rb +1 -1
- metadata +9 -144
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
require 'nokogiri'
|
|
2
|
+
require_relative 'xml_constraint_parsing'
|
|
3
|
+
require_relative 'xml_primitive_parsing'
|
|
4
|
+
require_relative 'xml_domain_type_parsing'
|
|
5
|
+
|
|
6
|
+
module OpenEHR
|
|
7
|
+
module Parser
|
|
8
|
+
# Reads the canonical openEHR ITS-XML archetype shape that
|
|
9
|
+
# XMLSerializer (lib/openehr/serializer/xml_serializer.rb) emits -
|
|
10
|
+
# see that file's header comment for how that shape was established
|
|
11
|
+
# as ground truth. Mirrors ADLParser#archetype's construction
|
|
12
|
+
# pattern (a small constructor-argument method per Archetype.new
|
|
13
|
+
# keyword) and shares the same <definition> constraint-tree readers
|
|
14
|
+
# as OPTParser via the 3 modules extracted in B3
|
|
15
|
+
# (xml_constraint_parsing/xml_primitive_parsing/xml_domain_type_parsing).
|
|
16
|
+
class XMLArchetypeParser < ::OpenEHR::Parser::Base
|
|
17
|
+
include XMLConstraintParsing
|
|
18
|
+
include XMLPrimitiveParsing
|
|
19
|
+
include XMLDomainTypeParsing
|
|
20
|
+
|
|
21
|
+
def parse
|
|
22
|
+
archetype
|
|
23
|
+
rescue OpenEHR::Parser::ParseError
|
|
24
|
+
raise
|
|
25
|
+
rescue StandardError => e
|
|
26
|
+
raise OpenEHR::Parser::ParseError, "invalid XML archetype (#{@filename}): #{e.class}: #{e.message}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def doc
|
|
32
|
+
@doc ||= begin
|
|
33
|
+
parsed = Nokogiri::XML::Document.parse(File.open(@filename, 'rb:bom|utf-8'))
|
|
34
|
+
parsed.remove_namespaces!
|
|
35
|
+
parsed
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def root
|
|
40
|
+
@root ||= doc.at('archetype')
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def text_on_path(xml, path)
|
|
44
|
+
node = xml.at(path)
|
|
45
|
+
node.nil? ? nil : node.text
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def code_phrase_from(node)
|
|
49
|
+
return nil if node.nil?
|
|
50
|
+
|
|
51
|
+
terminology_id = OpenEHR::RM::Support::Identification::TerminologyID.new(value: text_on_path(node, 'terminology_id/value'))
|
|
52
|
+
OpenEHR::RM::DataTypes::Text::CodePhrase.new(terminology_id: terminology_id, code_string: text_on_path(node, 'code_string'))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Nodes emitted with an 'id' attribute keying a plain text value
|
|
56
|
+
# (original_author/other_details/author/original_resource_uri) -
|
|
57
|
+
# the same shape everywhere it's used, so read generically.
|
|
58
|
+
def id_keyed_hash(nodes)
|
|
59
|
+
return nil if nodes.empty?
|
|
60
|
+
|
|
61
|
+
nodes.each_with_object({}) { |n, hash| hash[n['id']] = n.text }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def archetype_id
|
|
65
|
+
OpenEHR::RM::Support::Identification::ArchetypeID.new(value: text_on_path(root, 'archetype_id/value'))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def uid
|
|
69
|
+
value = text_on_path(root, 'uid/value')
|
|
70
|
+
value.nil? ? nil : OpenEHR::RM::Support::Identification::HierObjectID.new(value: value)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def adl_version
|
|
74
|
+
text_on_path(root, 'adl_version')
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def parent_archetype_id
|
|
78
|
+
value = text_on_path(root, 'parent_archetype_id/value')
|
|
79
|
+
value.nil? ? nil : OpenEHR::RM::Support::Identification::ArchetypeID.new(value: value)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def concept
|
|
83
|
+
text_on_path(root, 'concept')
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def original_language
|
|
87
|
+
code_phrase_from(root.at('original_language'))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def translations
|
|
91
|
+
translations_node = root.at('translations')
|
|
92
|
+
return nil if translations_node.nil?
|
|
93
|
+
|
|
94
|
+
translations_node.xpath('translation').each_with_object({}) do |node, hash|
|
|
95
|
+
hash[node['language']] = translation_details(node)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def translation_details(node)
|
|
100
|
+
OpenEHR::RM::Common::Resource::TranslationDetails.new(
|
|
101
|
+
language: code_phrase_from(node.at('language')),
|
|
102
|
+
author: id_keyed_hash(node.xpath('author')),
|
|
103
|
+
accreditation: text_on_path(node, 'accreditation'),
|
|
104
|
+
other_details: id_keyed_hash(node.xpath('other_details'))
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def description
|
|
109
|
+
node = root.at('description')
|
|
110
|
+
return nil if node.nil?
|
|
111
|
+
|
|
112
|
+
OpenEHR::RM::Common::Resource::ResourceDescription.new(
|
|
113
|
+
original_author: id_keyed_hash(node.xpath('original_author')),
|
|
114
|
+
other_contributors: description_other_contributors(node),
|
|
115
|
+
lifecycle_state: text_on_path(node, 'lifecycle_state'),
|
|
116
|
+
details: description_details(node)
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def description_other_contributors(node)
|
|
121
|
+
contributors = node.xpath('other_contributors').map(&:text)
|
|
122
|
+
contributors.empty? ? nil : contributors
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def description_details(node)
|
|
126
|
+
node.xpath('details/detail').each_with_object({}) do |detail, hash|
|
|
127
|
+
hash[detail['language']] = description_detail_item(detail)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def description_detail_item(node)
|
|
132
|
+
keywords = node.xpath('keywords').map(&:text)
|
|
133
|
+
OpenEHR::RM::Common::Resource::ResourceDescriptionItem.new(
|
|
134
|
+
language: code_phrase_from(node.at('language')),
|
|
135
|
+
purpose: text_on_path(node, 'purpose'),
|
|
136
|
+
keywords: keywords.empty? ? nil : keywords,
|
|
137
|
+
use: text_on_path(node, 'use'),
|
|
138
|
+
misuse: text_on_path(node, 'misuse'),
|
|
139
|
+
copyright: text_on_path(node, 'copyright'),
|
|
140
|
+
original_resource_uri: id_keyed_hash(node.xpath('original_resource_uri')),
|
|
141
|
+
other_details: id_keyed_hash(node.xpath('other_details'))
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Unlike OPTParser (whose top-level definition is a
|
|
146
|
+
# CArchetypeRoot, read via c_archetype_root, which has its own
|
|
147
|
+
# root-path override), a standalone archetype's <definition> root
|
|
148
|
+
# is a plain CComplexObject. c_complex_object (shared with
|
|
149
|
+
# OPTParser) always appends [node_id] to the path it's handed,
|
|
150
|
+
# which is correct for every *nested* complex object it's called
|
|
151
|
+
# on via attributes()/children() but wrong for the root itself
|
|
152
|
+
# (whose path must stay "/") - so the root is built directly here
|
|
153
|
+
# instead, mirroring c_archetype_root's override.
|
|
154
|
+
def definition
|
|
155
|
+
xml = root.at('definition')
|
|
156
|
+
node = Node.new
|
|
157
|
+
rm_type_name = text_on_path(xml, './rm_type_name')
|
|
158
|
+
node_id = text_on_path(xml, './node_id')
|
|
159
|
+
node.id = node_id unless node_id.nil? || node_id.empty?
|
|
160
|
+
node.path = '/'
|
|
161
|
+
OpenEHR::AM::Archetype::ConstraintModel::CComplexObject.new(
|
|
162
|
+
rm_type_name: rm_type_name, node_id: node.id, path: node.path,
|
|
163
|
+
occurrences: occurrences(xml.xpath('./occurrences')), attributes: attributes(xml.xpath('./attributes'), node)
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def invariants
|
|
168
|
+
node = root.at('invariants')
|
|
169
|
+
return nil if node.nil?
|
|
170
|
+
|
|
171
|
+
node.xpath('invariant').map { |invariant| invariant_assertion(invariant) }
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def invariant_assertion(node)
|
|
175
|
+
assertions(node, Node.new).first
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def ontology
|
|
179
|
+
node = root.at('ontology')
|
|
180
|
+
OpenEHR::AM::Archetype::Ontology::ArchetypeOntology.new(
|
|
181
|
+
primary_language: text_on_path(node, 'primary_language'),
|
|
182
|
+
specialisation_depth: ontology_specialisation_depth(node),
|
|
183
|
+
languages_available: ontology_string_list(node, 'languages_available'),
|
|
184
|
+
terminologies_available: ontology_string_list(node, 'terminologies_available'),
|
|
185
|
+
term_definitions: ontology_term_definitions(node, 'term_definitions'),
|
|
186
|
+
constraint_definitions: ontology_optional_term_definitions(node, 'constraint_definitions'),
|
|
187
|
+
term_bindings: ontology_bindings(node, 'term_bindings') { |value| [code_phrase_from_binding(value)] },
|
|
188
|
+
constraint_bindings: ontology_bindings(node, 'constraint_bindings') { |value| OpenEHR::RM::DataTypes::URI::DvUri.new(value: value) }
|
|
189
|
+
)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def ontology_specialisation_depth(node)
|
|
193
|
+
value = text_on_path(node, 'specialisation_depth')
|
|
194
|
+
value.nil? ? nil : value.to_i
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def ontology_string_list(node, keyword)
|
|
198
|
+
values = node.xpath(keyword).map(&:text)
|
|
199
|
+
values.empty? ? nil : values
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def ontology_term_definitions(node, keyword)
|
|
203
|
+
node.xpath(keyword).each_with_object({}) do |term_node, by_lang|
|
|
204
|
+
lang = term_node['language']
|
|
205
|
+
(by_lang[lang] ||= {})[term_node['code']] = archetype_term(term_node)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def ontology_optional_term_definitions(node, keyword)
|
|
210
|
+
result = ontology_term_definitions(node, keyword)
|
|
211
|
+
result.empty? ? nil : result
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def archetype_term(term_node)
|
|
215
|
+
items = term_node.xpath('items').each_with_object({}) { |item, hash| hash[item['id']] = item.text }
|
|
216
|
+
OpenEHR::AM::Archetype::Ontology::ArchetypeTerm.new(code: term_node['code'], items: items)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def ontology_bindings(node, keyword)
|
|
220
|
+
result = node.xpath(keyword).each_with_object({}) do |binding_node, by_terminology|
|
|
221
|
+
terminology = binding_node['terminology']
|
|
222
|
+
(by_terminology[terminology] ||= {})[binding_node['code']] = yield(binding_node.text)
|
|
223
|
+
end
|
|
224
|
+
result.empty? ? nil : result
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# term_bindings' value is a "terminology::code" qualified
|
|
228
|
+
# reference (matching ADLSerializer/XMLSerializer's own emission),
|
|
229
|
+
# not a bare code - split it back into a CodePhrase.
|
|
230
|
+
def code_phrase_from_binding(value)
|
|
231
|
+
terminology, code = value.split('::', 2)
|
|
232
|
+
OpenEHR::RM::DataTypes::Text::CodePhrase.new(
|
|
233
|
+
terminology_id: OpenEHR::RM::Support::Identification::TerminologyID.new(value: terminology),
|
|
234
|
+
code_string: code
|
|
235
|
+
)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def archetype
|
|
239
|
+
OpenEHR::AM::Archetype::Archetype.new(
|
|
240
|
+
archetype_id: archetype_id,
|
|
241
|
+
adl_version: adl_version,
|
|
242
|
+
uid: uid,
|
|
243
|
+
concept: concept,
|
|
244
|
+
original_language: original_language,
|
|
245
|
+
translations: translations,
|
|
246
|
+
description: description,
|
|
247
|
+
definition: definition,
|
|
248
|
+
ontology: ontology,
|
|
249
|
+
parent_archetype_id: parent_archetype_id,
|
|
250
|
+
invariants: invariants
|
|
251
|
+
)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
module OpenEHR
|
|
2
|
+
module Parser
|
|
3
|
+
# Structural/constraint-tree node builders (C_COMPLEX_OBJECT,
|
|
4
|
+
# C_ATTRIBUTE, C_PRIMITIVE_OBJECT, ARCHETYPE_SLOT, ARCHETYPE_INTERNAL_REF,
|
|
5
|
+
# CONSTRAINT_REF, ASSERTION) shared between OPTParser and
|
|
6
|
+
# XMLArchetypeParser - both read the same canonical shape (see
|
|
7
|
+
# lib/openehr/serializer/xml_serializer.rb's header comment for how
|
|
8
|
+
# that shape was established as ground truth against real .opt
|
|
9
|
+
# fixtures). Extracted from opt_parser.rb without behavior changes,
|
|
10
|
+
# except: expr_unary_operator support and tag reading in assertions()
|
|
11
|
+
# were both added here since XMLSerializer already emits them, and
|
|
12
|
+
# occurrences()/numeric bounds are now real-aware where the caller
|
|
13
|
+
# asks for it (C_REAL ranges were silently truncated to Integer via
|
|
14
|
+
# String#to_i before this).
|
|
15
|
+
module XMLConstraintParsing
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def c_archetype_root(xml, node = Node.new)
|
|
19
|
+
rm_type_name = text_on_path(xml, './rm_type_name')
|
|
20
|
+
id = text_on_path(xml, './node_id')
|
|
21
|
+
node.id = id unless id.nil? or id.empty?
|
|
22
|
+
occurrences = occurrences(xml.xpath('./occurrences'))
|
|
23
|
+
archetype_id = OpenEHR::RM::Support::Identification::ArchetypeID.new(value: text_on_path(xml, './archetype_id/value'))
|
|
24
|
+
if node.root? or node.id.nil?
|
|
25
|
+
node.path = "/"
|
|
26
|
+
end
|
|
27
|
+
component_terminologies(archetype_id, xml)
|
|
28
|
+
OpenEHR::AM::Archetype::ConstraintModel::CArchetypeRoot.new(rm_type_name: rm_type_name, node_id: node.id, path: node.path, occurrences: occurrences, archetype_id: archetype_id, attributes: attributes(xml.xpath('./attributes'), node))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def c_complex_object(xml, node)
|
|
32
|
+
rm_type_name = xml.xpath('./rm_type_name').text
|
|
33
|
+
node_id = xml.xpath('./node_id').text
|
|
34
|
+
unless node_id.nil? or node_id.empty?
|
|
35
|
+
node.id = node_id
|
|
36
|
+
node.path = "#{node.path}[#{node.id}]"
|
|
37
|
+
end
|
|
38
|
+
OpenEHR::AM::Archetype::ConstraintModel::CComplexObject.new(rm_type_name: rm_type_name, node_id: node.id, path: node.path, occurrences: occurrences(xml.xpath('./occurrences')), attributes: attributes(xml.xpath('./attributes'), node))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def attributes(attributes_xml, node)
|
|
42
|
+
attributes_xml.map do |attr|
|
|
43
|
+
rm_attribute_name = attr.at('rm_attribute_name').text
|
|
44
|
+
if node.root?
|
|
45
|
+
path = "/#{rm_attribute_name}"
|
|
46
|
+
else
|
|
47
|
+
path = "#{node.path}/#{rm_attribute_name}"
|
|
48
|
+
end
|
|
49
|
+
child_node = Node.new(node)
|
|
50
|
+
child_node.path = path
|
|
51
|
+
child_node.id = node.id
|
|
52
|
+
send attr.attributes['type'].text.downcase, attr, child_node
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Each sibling child gets its own Node, copied fresh from the
|
|
57
|
+
# attribute's node - c_complex_object mutates node.path/node.id
|
|
58
|
+
# in place when a node_id is present, so sharing one Node across
|
|
59
|
+
# a map() here would leak sibling A's path into sibling B (e.g. a
|
|
60
|
+
# C_MULTIPLE_ATTRIBUTE with 2+ C_COMPLEX_OBJECT children, each
|
|
61
|
+
# with its own node_id, would produce "/items[a][b]" instead of
|
|
62
|
+
# "/items[a]" and "/items[b]").
|
|
63
|
+
def children(children_xml, node)
|
|
64
|
+
children_xml.map do |child|
|
|
65
|
+
child_node = Node.new(node)
|
|
66
|
+
child_node.path = node.path
|
|
67
|
+
child_node.id = node.id
|
|
68
|
+
send child.attributes['type'].text.downcase, child, child_node
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def c_single_attribute(attr_xml, node)
|
|
73
|
+
rm_attribute_name = attr_xml.at('rm_attribute_name').text
|
|
74
|
+
existence = occurrences(attr_xml.at('existence'))
|
|
75
|
+
OpenEHR::AM::Archetype::ConstraintModel::CSingleAttribute.new(rm_attribute_name: rm_attribute_name, existence: existence, path: node.path, children: children(attr_xml.xpath('./children'), node))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def c_multiple_attribute(attr_xml, node)
|
|
79
|
+
rm_attribute_name = attr_xml.at('rm_attribute_name').text
|
|
80
|
+
existence = occurrences(attr_xml.at('existence'))
|
|
81
|
+
OpenEHR::AM::Archetype::ConstraintModel::CMultipleAttribute.new(rm_attribute_name: rm_attribute_name, existence: existence, path: node.path, cardinality: cardinality(attr_xml), children: children(attr_xml.xpath('./children'), node))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def archetype_slot(attr_xml, node)
|
|
85
|
+
node_id = attr_xml.at('node_id').text
|
|
86
|
+
node.id = node_id
|
|
87
|
+
# Matches c_complex_object's path convention: a slot's own
|
|
88
|
+
# node_id belongs in its path (e.g. "/items[at0053]"), not just
|
|
89
|
+
# its parent attribute's path - without this, two slots under
|
|
90
|
+
# the same C_MULTIPLE_ATTRIBUTE would collapse to one path.
|
|
91
|
+
node.path = "#{node.path}[#{node.id}]" unless node_id.nil? || node_id.empty?
|
|
92
|
+
rm_type_name = attr_xml.at('rm_type_name').text
|
|
93
|
+
occurrences = occurrences(attr_xml.at('occurrences'))
|
|
94
|
+
includes_leaf = attr_xml.at('includes')
|
|
95
|
+
includes = assertions(includes_leaf.children, node) if includes_leaf
|
|
96
|
+
excludes_leaf = attr_xml.at('excludes')
|
|
97
|
+
excludes = assertions(excludes_leaf.children, node) if excludes_leaf
|
|
98
|
+
OpenEHR::AM::Archetype::ConstraintModel::ArchetypeSlot.new(path: node.path, node_id: node.id, rm_type_name: rm_type_name, occurrences: occurrences, includes: includes, excludes: excludes)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def occurrences(occurrence_xml)
|
|
102
|
+
numeric_interval(occurrence_xml, real: false)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Shared by occurrences()/existence/cardinality (always Integer
|
|
106
|
+
# bounds) and C_REAL's range (Real bounds, via numeric_interval's
|
|
107
|
+
# real: true - see xml_primitive_parsing.rb's c_real).
|
|
108
|
+
def numeric_interval(occurrence_xml, real:)
|
|
109
|
+
return nil if occurrence_xml.nil?
|
|
110
|
+
|
|
111
|
+
lower_node = occurrence_xml.at('lower')
|
|
112
|
+
upper_node = occurrence_xml.at('upper')
|
|
113
|
+
lower_included_node = occurrence_xml.at('lower_included')
|
|
114
|
+
upper_included_node = occurrence_xml.at('upper_included')
|
|
115
|
+
lower_unbounded_node = occurrence_xml.at('lower_unbounded')
|
|
116
|
+
upper_unbounded_node = occurrence_xml.at('upper_unbounded')
|
|
117
|
+
|
|
118
|
+
lower = lower_node ? numeric_bound(lower_node.text, real) : nil
|
|
119
|
+
upper = upper_node ? numeric_bound(upper_node.text, real) : nil
|
|
120
|
+
lower_included = lower_included_node ? to_bool(lower_included_node.text) : (lower.nil? ? nil : true)
|
|
121
|
+
upper_included = upper_included_node ? to_bool(upper_included_node.text) : (upper.nil? ? nil : true)
|
|
122
|
+
lower_unbounded = lower_unbounded_node ? to_bool(lower_unbounded_node.text) : false
|
|
123
|
+
upper_unbounded = upper_unbounded_node ? to_bool(upper_unbounded_node.text) : false
|
|
124
|
+
|
|
125
|
+
# An occurrences element with none of its children present carries no
|
|
126
|
+
# constraint at all; Interval requires at least one bound, so treat
|
|
127
|
+
# this as "no occurrences data" rather than raising.
|
|
128
|
+
return nil if lower.nil? && upper.nil? && !lower_unbounded && !upper_unbounded
|
|
129
|
+
|
|
130
|
+
# Handle unbounded intervals properly
|
|
131
|
+
if upper_unbounded || upper.nil?
|
|
132
|
+
upper = nil
|
|
133
|
+
upper_included = nil
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
if lower_unbounded || lower.nil?
|
|
137
|
+
lower = nil
|
|
138
|
+
lower_included = nil
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
OpenEHR::AssumedLibraryTypes::Interval.new(
|
|
142
|
+
lower: lower,
|
|
143
|
+
upper: upper,
|
|
144
|
+
lower_included: lower_included,
|
|
145
|
+
upper_included: upper_included
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def numeric_bound(text, real)
|
|
150
|
+
real ? text.to_f : text.to_i
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def cardinality(xml)
|
|
154
|
+
return nil if xml.nil?
|
|
155
|
+
|
|
156
|
+
order_node = xml.at('is_ordered')
|
|
157
|
+
unique_node = xml.at('is_unique')
|
|
158
|
+
interval_node = xml.at('interval')
|
|
159
|
+
|
|
160
|
+
# No cardinality sub-elements at all means no cardinality data.
|
|
161
|
+
return nil if order_node.nil? && unique_node.nil? && interval_node.nil?
|
|
162
|
+
|
|
163
|
+
order = order_node ? to_bool(order_node.text) : false
|
|
164
|
+
unique = unique_node ? to_bool(unique_node.text) : false
|
|
165
|
+
interval = interval_node ? occurrences(interval_node) : nil
|
|
166
|
+
|
|
167
|
+
OpenEHR::AM::Archetype::ConstraintModel::Cardinality.new(
|
|
168
|
+
is_ordered: order,
|
|
169
|
+
is_unique: unique,
|
|
170
|
+
interval: interval
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def archetype_internal_ref(attr_xml, node)
|
|
175
|
+
rm_type_name = attr_xml.at('rm_type_name').text
|
|
176
|
+
target_path = attr_xml.at('target_path').text
|
|
177
|
+
occurrences = occurrences(attr_xml.at('occurrences'))
|
|
178
|
+
OpenEHR::AM::Archetype::ConstraintModel::ArchetypeInternalRef.new(rm_type_name: rm_type_name, occurrences: occurrences, target_path: target_path)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def constraint_ref(attr_xml, node)
|
|
182
|
+
rm_type_name = attr_xml.at('rm_type_name').text
|
|
183
|
+
reference = attr_xml.at('reference').text
|
|
184
|
+
occurrences = occurrences(attr_xml.at('occurrences'))
|
|
185
|
+
OpenEHR::AM::Archetype::ConstraintModel::ConstraintRef.new(rm_type_name: rm_type_name, occurrences: occurrences, reference: reference)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def assertions(attr_xml, node)
|
|
189
|
+
tag_node = attr_xml.at('tag')
|
|
190
|
+
tag = tag_node.nil? ? nil : tag_node.text
|
|
191
|
+
string_expression = attr_xml.at('string_expression')
|
|
192
|
+
string_expression = string_expression.nil? ? nil : string_expression.text
|
|
193
|
+
expression_leaf = attr_xml.at 'expression'
|
|
194
|
+
expression = send expression_leaf.attributes['type'].text.downcase, expression_leaf
|
|
195
|
+
[OpenEHR::AM::Archetype::Assertion::Assertion.new(tag: tag, expression: expression, string_expression: string_expression)]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def expr_binary_operator(attr_xml)
|
|
199
|
+
type = attr_xml.at('type').text
|
|
200
|
+
operator = OpenEHR::AM::Archetype::Assertion::OperatorKind.new(value: attr_xml.at('operator').text.to_i)
|
|
201
|
+
|
|
202
|
+
precedence_overridden = attr_xml.at('precedence_overridden').text == 'true' ? true : false
|
|
203
|
+
right_operand_leaf = attr_xml.at 'right_operand'
|
|
204
|
+
right_operand = send right_operand_leaf.attributes['type'].text.downcase, right_operand_leaf
|
|
205
|
+
left_operand_leaf = attr_xml.at 'left_operand'
|
|
206
|
+
left_operand = send left_operand_leaf.attributes['type'].text.downcase, left_operand_leaf
|
|
207
|
+
OpenEHR::AM::Archetype::Assertion::ExprBinaryOperator.new(type: type, operator: operator, precedence_overridden: precedence_overridden, right_operand: right_operand, left_operand: left_operand)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def expr_unary_operator(attr_xml)
|
|
211
|
+
type = attr_xml.at('type').text
|
|
212
|
+
operator = OpenEHR::AM::Archetype::Assertion::OperatorKind.new(value: attr_xml.at('operator').text.to_i)
|
|
213
|
+
precedence_overridden = attr_xml.at('precedence_overridden').text == 'true' ? true : false
|
|
214
|
+
operand_leaf = attr_xml.at 'operand'
|
|
215
|
+
operand = send operand_leaf.attributes['type'].text.downcase, operand_leaf
|
|
216
|
+
OpenEHR::AM::Archetype::Assertion::ExprUnaryOperator.new(type: type, operator: operator, precedence_overridden: precedence_overridden, operand: operand)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def expr_leaf(attr_xml)
|
|
220
|
+
type = attr_xml.at('type').text
|
|
221
|
+
item_leaf = attr_xml.at('item')
|
|
222
|
+
item = send type.downcase, item_leaf
|
|
223
|
+
reference_type = attr_xml.at('reference_type').text
|
|
224
|
+
OpenEHR::AM::Archetype::Assertion::ExprLeaf.new(type: type, item: item, reference_type: reference_type)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def c_primitive_object(attr_xml, node)
|
|
228
|
+
rm_type_name = attr_xml.at('rm_type_name').text
|
|
229
|
+
occurrences = occurrences(attr_xml.at('occurrences'))
|
|
230
|
+
item = send attr_xml.at('item')['type'].downcase, attr_xml.at('item')
|
|
231
|
+
OpenEHR::AM::Archetype::ConstraintModel::CPrimitiveObject.new(rm_type_name: rm_type_name, occurrences: occurrences, node_id: node.id, item: item)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Bare-literal ExprLeaf#item readers (type "String"/"Integer"/
|
|
235
|
+
# "Real"/"Boolean", reference_type "CONSTANT") - distinct from
|
|
236
|
+
# the C_STRING/C_INTEGER/... constraint-item readers in
|
|
237
|
+
# xml_primitive_parsing.rb, which are dispatched to for
|
|
238
|
+
# reference_type "constraint" leaves instead.
|
|
239
|
+
def string(attr_xml)
|
|
240
|
+
attr_xml.text
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def integer(attr_xml)
|
|
244
|
+
attr_xml.text.to_i
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def real(attr_xml)
|
|
248
|
+
attr_xml.text.to_f
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def boolean(attr_xml)
|
|
252
|
+
to_bool(attr_xml.text)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def to_bool(str)
|
|
256
|
+
return nil if str.nil?
|
|
257
|
+
str = str.text if str.respond_to?(:text)
|
|
258
|
+
return true if /true/i =~ str.to_s
|
|
259
|
+
return false if /false/i =~ str.to_s
|
|
260
|
+
nil
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
module OpenEHR
|
|
2
|
+
module Parser
|
|
3
|
+
# openEHR Archetype Profile domain-type readers (C_CODE_PHRASE,
|
|
4
|
+
# C_DV_QUANTITY, C_DV_ORDINAL, C_DV_SCALE, C_DV_STATE), shared
|
|
5
|
+
# between OPTParser and XMLArchetypeParser. Extracted from
|
|
6
|
+
# opt_parser.rb without behavior changes, except: C_DV_QUANTITY now
|
|
7
|
+
# reads assumed_value (previously ignored) and its magnitude range
|
|
8
|
+
# reads Float bounds instead of Integer (precision stays Integer -
|
|
9
|
+
# see numeric_interval in xml_constraint_parsing.rb).
|
|
10
|
+
module XMLDomainTypeParsing
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def c_code_phrase(attr_xml, node)
|
|
14
|
+
terminology_id_node = attr_xml.at('terminology_id/value')
|
|
15
|
+
terminology_id = terminology_id_node ? OpenEHR::RM::Support::Identification::TerminologyID.new(value: terminology_id_node.text.strip) : nil
|
|
16
|
+
|
|
17
|
+
code_list_nodes = attr_xml.xpath('code_list')
|
|
18
|
+
code_list = code_list_nodes.map { |code_node| code_node.text.strip }
|
|
19
|
+
code_list = [code_list.first] if code_list.size == 1 && code_list.first.empty?
|
|
20
|
+
|
|
21
|
+
occurrences_node = attr_xml.at('occurrences')
|
|
22
|
+
occurrences_obj = occurrences_node ? occurrences(occurrences_node) : nil
|
|
23
|
+
|
|
24
|
+
OpenEHR::AM::OpenEHRProfile::DataTypes::Text::CCodePhrase.new(
|
|
25
|
+
terminology_id: terminology_id,
|
|
26
|
+
code_list: code_list,
|
|
27
|
+
path: node.path,
|
|
28
|
+
occurrences: occurrences_obj,
|
|
29
|
+
rm_type_name: 'CODE_PHRASE'
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The <property> element is optional in real templates; return nil rather
|
|
34
|
+
# than dereferencing missing terminology/code nodes.
|
|
35
|
+
def property_code_phrase(property_xml)
|
|
36
|
+
return nil if property_xml.nil?
|
|
37
|
+
terminology_node = property_xml.at('terminology_id/value')
|
|
38
|
+
code_node = property_xml.at('code_string')
|
|
39
|
+
return nil if terminology_node.nil? || code_node.nil?
|
|
40
|
+
terminology_id = OpenEHR::RM::Support::Identification::TerminologyID.new(value: terminology_node.text)
|
|
41
|
+
OpenEHR::RM::DataTypes::Text::CodePhrase.new(terminology_id: terminology_id, code_string: code_node.text)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def c_dv_quantity(attr_xml, node)
|
|
45
|
+
rm_type_name = attr_xml.at('rm_type_name').text
|
|
46
|
+
occurrences = occurrences(attr_xml.at('occurrences'))
|
|
47
|
+
property = property_code_phrase(attr_xml.at('property'))
|
|
48
|
+
list = attr_xml.xpath('.//list').map { |element| c_quantity_item(element) }
|
|
49
|
+
assumed_value = dv_quantity_assumed_value(attr_xml.at('assumed_value'))
|
|
50
|
+
OpenEHR::AM::OpenEHRProfile::DataTypes::Quantity::CDvQuantity.new(
|
|
51
|
+
rm_type_name: rm_type_name, occurrences: occurrences, list: list, property: property, assumed_value: assumed_value
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def c_quantity_item(element)
|
|
56
|
+
units = element.at('units').text if element.at('units')
|
|
57
|
+
magnitude = numeric_interval(element.at('magnitude'), real: true) if element.at('magnitude')
|
|
58
|
+
precision = numeric_interval(element.at('precision'), real: false) if element.at('precision')
|
|
59
|
+
OpenEHR::AM::OpenEHRProfile::DataTypes::Quantity::CQuantityItem.new(magnitude: magnitude, precision: precision, units: units)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# assumed_value is a real DV_QUANTITY (plain magnitude/precision,
|
|
63
|
+
# not a range), matching XMLSerializer#emit_dv_quantity_assumed_value.
|
|
64
|
+
def dv_quantity_assumed_value(element)
|
|
65
|
+
return nil if element.nil?
|
|
66
|
+
|
|
67
|
+
units = element.at('units')&.text
|
|
68
|
+
magnitude_node = element.at('magnitude')
|
|
69
|
+
magnitude = magnitude_node ? magnitude_node.text.to_f : nil
|
|
70
|
+
precision_node = element.at('precision')
|
|
71
|
+
precision = precision_node ? precision_node.text.to_i : nil
|
|
72
|
+
OpenEHR::RM::DataTypes::Quantity::DvQuantity.new(units: units, magnitude: magnitude, precision: precision)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def c_dv_ordinal(attr_xml, node)
|
|
76
|
+
rm_type_name = attr_xml.at('rm_type_name').text
|
|
77
|
+
occurrences = occurrences(attr_xml.at('occurrences'))
|
|
78
|
+
list = attr_xml.xpath('list').map { |element| dv_ordinal_item(element) }.compact
|
|
79
|
+
OpenEHR::AM::OpenEHRProfile::DataTypes::Quantity::CDvOrdinal.new(rm_type_name: rm_type_name, occurrences: occurrences, list: list)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# DV_ORDINAL.symbol is spec'd as DV_CODED_TEXT; the OPT XML only
|
|
83
|
+
# carries a defining_code (terminology_id + code_string), so the
|
|
84
|
+
# DvCodedText's own value is set to that same code_string (there
|
|
85
|
+
# is no separate display text in this element).
|
|
86
|
+
def dv_ordinal_item(element)
|
|
87
|
+
value_node = element.at('value')
|
|
88
|
+
return nil unless value_node && !value_node.text.empty?
|
|
89
|
+
|
|
90
|
+
code_phrase = property_code_phrase(element.at('symbol/defining_code'))
|
|
91
|
+
return nil if code_phrase.nil?
|
|
92
|
+
|
|
93
|
+
symbol = OpenEHR::RM::DataTypes::Text::DvCodedText.new(value: code_phrase.code_string, defining_code: code_phrase)
|
|
94
|
+
OpenEHR::RM::DataTypes::Quantity::DvOrdinal.new(value: value_node.text.to_i, symbol: symbol)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def c_dv_scale(attr_xml, node)
|
|
98
|
+
rm_type_name = attr_xml.at('rm_type_name').text
|
|
99
|
+
occurrences = occurrences(attr_xml.at('occurrences'))
|
|
100
|
+
list = attr_xml.xpath('list').map { |element| dv_scale_item(element) }.compact
|
|
101
|
+
OpenEHR::AM::OpenEHRProfile::DataTypes::Quantity::CDvScale.new(rm_type_name: rm_type_name, occurrences: occurrences, list: list)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Same XML shape as C_DV_ORDINAL's list items, but DV_SCALE.value
|
|
105
|
+
# is Real rather than Integer.
|
|
106
|
+
def dv_scale_item(element)
|
|
107
|
+
value_node = element.at('value')
|
|
108
|
+
return nil unless value_node && !value_node.text.empty?
|
|
109
|
+
|
|
110
|
+
code_phrase = property_code_phrase(element.at('symbol/defining_code'))
|
|
111
|
+
return nil if code_phrase.nil?
|
|
112
|
+
|
|
113
|
+
symbol = OpenEHR::RM::DataTypes::Text::DvCodedText.new(value: code_phrase.code_string, defining_code: code_phrase)
|
|
114
|
+
OpenEHR::RM::DataTypes::Quantity::DvScale.new(value: value_node.text.to_f, symbol: symbol)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# No .opt fixture in this gem's corpus uses a C_DV_STATE (state
|
|
118
|
+
# machine) constraint, so its actual OPT XML shape is unverified,
|
|
119
|
+
# and XMLSerializer itself can't emit one to a standalone
|
|
120
|
+
# archetype either (ADL 1.4 has no grammar rule for it) - raising
|
|
121
|
+
# a clear, documented error here is safer than guessing at
|
|
122
|
+
# element names and risking a silently wrong StateMachine.
|
|
123
|
+
def c_dv_state(_attr_xml, _node)
|
|
124
|
+
raise NotImplementedError, 'OPTParser does not yet support C_DV_STATE (state machine) constraints'
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|