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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.rdoc +19 -6
  3. data/lib/openehr/am/archetype/constraint_model.rb +1 -1
  4. data/lib/openehr/aql/engine/contains_resolver.rb +98 -7
  5. data/lib/openehr/aql/engine/dataset.rb +3 -1
  6. data/lib/openehr/aql/engine/predicate_evaluator.rb +70 -3
  7. data/lib/openehr/aql/engine.rb +45 -12
  8. data/lib/openehr/assumed_library_types.rb +20 -17
  9. data/lib/openehr/parser/adl_parser.rb +0 -4
  10. data/lib/openehr/parser/opt_parser.rb +12 -429
  11. data/lib/openehr/parser/xml_archetype_parser.rb +255 -0
  12. data/lib/openehr/parser/xml_constraint_parsing.rb +264 -0
  13. data/lib/openehr/parser/xml_domain_type_parsing.rb +128 -0
  14. data/lib/openehr/parser/xml_primitive_parsing.rb +145 -0
  15. data/lib/openehr/parser.rb +1 -0
  16. data/lib/openehr/rm/common/generic.rb +6 -9
  17. data/lib/openehr/rm/common/resource.rb +0 -1
  18. data/lib/openehr/rm/composition/content/entry.rb +4 -1
  19. data/lib/openehr/rm/data_structures/item_structure/representation.rb +12 -2
  20. data/lib/openehr/rm/data_structures/item_structure.rb +0 -1
  21. data/lib/openehr/rm/data_types/basic.rb +11 -21
  22. data/lib/openehr/rm/data_types/encapsulated.rb +18 -2
  23. data/lib/openehr/rm/data_types/quantity/date_time.rb +203 -28
  24. data/lib/openehr/rm/data_types/quantity.rb +66 -10
  25. data/lib/openehr/rm/data_types/time_specification.rb +101 -27
  26. data/lib/openehr/rm/data_types/uri.rb +0 -8
  27. data/lib/openehr/rm/ehr.rb +6 -6
  28. data/lib/openehr/rm/factory.rb +22 -0
  29. data/lib/openehr/rm/security.rb +3 -1
  30. data/lib/openehr/rm/type_name.rb +0 -1
  31. data/lib/openehr/serializer/adl_serializer.rb +239 -84
  32. data/lib/openehr/serializer/base.rb +2 -0
  33. data/lib/openehr/serializer/rm_json_serializer.rb +0 -1
  34. data/lib/openehr/serializer/xml_serializer.rb +351 -103
  35. data/lib/openehr/version.rb +1 -1
  36. 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