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
@@ -1,64 +1,58 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rexml/document'
2
4
  require 'builder'
3
5
  require_relative 'base'
4
6
 
5
7
  module OpenEHR
6
8
  module Serializer
9
+ # Emits the canonical openEHR ITS-XML AOM 1.4 shape for the parts
10
+ # that have a verifiable ground truth: <occurrences> (plural),
11
+ # xsi:type dispatch on <attributes>/<children>, nested
12
+ # terminology_id/code_list, and real ASSERTION expression trees.
13
+ # This gem's own OPTParser (lib/openehr/parser/opt_parser.rb) reads
14
+ # exactly this shape for the <definition> constraint tree - verified
15
+ # against real Ocean Template Designer-generated .opt fixtures in
16
+ # spec/lib/openehr/opt_parser/ - so the two no longer contradict
17
+ # each other. <ontology>'s attribute-style term_definitions follows
18
+ # the same confirmed-from-.opt convention; term_bindings/
19
+ # constraint_bindings and the header extras (uid/translations/etc.)
20
+ # have no directly-comparable real fixture in this repo, so they
21
+ # follow the same attribute-style spirit for internal consistency -
22
+ # XMLArchetypeParser (lib/openehr/parser/xml_archetype_parser.rb) is
23
+ # the authority for reading them back.
7
24
  class XMLSerializer < BaseSerializer
8
25
  def header
9
- header = ''
26
+ header = +''
10
27
  xml = Builder::XmlMarkup.new(:indent => 2, :target => header)
11
- xml.archetype_id do
12
- xml.value @archetype.archetype_id.value
13
- end
28
+ xml.archetype_id { xml.value @archetype.archetype_id.value }
29
+ xml.uid { xml.value @archetype.uid.value } if @archetype.uid
30
+ xml.adl_version @archetype.adl_version if @archetype.adl_version
31
+ xml.parent_archetype_id { xml.value @archetype.parent_archetype_id.value } if @archetype.parent_archetype_id
14
32
  xml.concept @archetype.concept
15
33
  xml.original_language do
16
- xml.terminology_id do
17
- xml.value @archetype.original_language.terminology_id.value
18
- end
34
+ xml.terminology_id { xml.value @archetype.original_language.terminology_id.value }
19
35
  xml.code_string @archetype.original_language.code_string
20
36
  end
37
+ emit_translations(xml, @archetype.translations)
21
38
  return header
22
39
  end
23
40
 
24
41
  def description
25
- desc = ''
42
+ desc = +''
26
43
  xml = Builder::XmlMarkup.new(:indent => 2, :target => desc)
27
44
  ad = @archetype.description
28
45
  if ad
29
46
  xml.description do
30
- ad.original_author.each do |key,value|
31
- xml.original_author(value,"id"=>key)
32
- end
33
- if ad.other_contributors
34
- ad.other_contributors.each do |co|
35
- xml.other_contributors co
36
- end
37
- end
47
+ ad.original_author.each { |key, value| xml.original_author(value, 'id' => key) }
48
+ (ad.other_contributors || []).each { |co| xml.other_contributors co }
38
49
  xml.lifecycle_state ad.lifecycle_state
39
50
  xml.details do
40
- ad.details.each do |lang, item|
41
- xml.language do
42
- xml.terminology_id do
43
- xml.value item.language.terminology_id.value
44
- end
45
- xml.code_string lang
46
- end
47
- xml.purpose item.purpose
48
- if item.keywords then
49
- item.keywords.each do |word|
50
- xml.keywords word
51
- end
52
- end
53
- xml.use item.use if item.use
54
- xml.misuse item.misuse if item.misuse
55
- xml.copyright item.copyright if item.copyright
56
- if ad.other_details
57
- ad.other_details.each do |key,value|
58
- xml.other_details(value, "id"=>key)
59
- end
60
- end
61
- end
51
+ # Each language's fields are wrapped in its own <detail
52
+ # language="..."> - without this, multiple languages would
53
+ # flatten into one <details> with no way to tell which
54
+ # purpose/keywords/etc. belong to which language.
55
+ ad.details.each { |lang, item| xml.tag!('detail', 'language' => lang) { emit_description_detail(xml, lang, item) } }
62
56
  end
63
57
  end
64
58
  end
@@ -66,65 +60,108 @@ module OpenEHR
66
60
  end
67
61
 
68
62
  def definition
69
- definition = ''
63
+ definition = +''
70
64
  xml = Builder::XmlMarkup.new(:indent => 2, :target => definition)
71
- xml.definition do
72
- emit_c_object_body(xml, @archetype.definition)
73
- end
65
+ xml.definition { emit_c_object_body(xml, @archetype.definition) }
74
66
  return definition
75
67
  end
76
68
 
77
69
  def ontology
78
- ontology = ''
70
+ ontology = +''
79
71
  ao = @archetype.ontology
80
72
  xml = Builder::XmlMarkup.new(:indent => 2, :target => ontology)
81
73
  xml.ontology do
74
+ xml.primary_language ao.primary_language if ao.primary_language
82
75
  xml.specialisation_depth ao.specialisation_depth
83
76
  (ao.languages_available || []).each { |lang| xml.languages_available lang }
84
77
  (ao.terminologies_available || []).each { |t| xml.terminologies_available t }
85
- xml.term_definitions do
86
- ao.term_definitions.each do |lang, terms|
87
- xml.language lang
88
- xml.terms do
89
- terms.each do |code, term|
90
- xml.code code
91
- xml.items do
92
- term.items.each do |key, value|
93
- xml.item do
94
- xml.key key
95
- xml.value value
96
- end
97
- end
98
- end
99
- end
100
- end
101
- end
102
- end
103
- emit_term_bindings(xml, ao.term_bindings)
78
+ emit_term_definitions(xml, 'term_definitions', ao.term_definitions)
79
+ emit_term_definitions(xml, 'constraint_definitions', ao.constraint_definitions) if ao.constraint_definitions
80
+ emit_bindings(xml, 'term_bindings', ao.term_bindings) { |cp| "#{cp.terminology_id.value}::#{cp.code_string}" }
81
+ emit_bindings(xml, 'constraint_bindings', ao.constraint_bindings) { |uri| uri.value }
104
82
  end
105
83
  end
106
84
 
107
85
  def merge
108
86
  archetype = "<?xml version='1.0' encoding='UTF-8'?>" + NL +
109
87
  "<archetype xmlns=\"http://schemas.openehr.org/v1\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">" + NL +
110
- header + description + definition +
111
- ontology + '</archetype>'
88
+ header + description + definition + emit_invariants + ontology + '</archetype>'
112
89
  return archetype
113
90
  end
114
91
 
115
92
  include OpenEHR::AM::Archetype::ConstraintModel
116
93
  Primitive = OpenEHR::AM::Archetype::ConstraintModel::Primitive
94
+ AssertionModel = OpenEHR::AM::Archetype::Assertion
117
95
  OpenEHRProfile = OpenEHR::AM::OpenEHRProfile
118
96
 
97
+ PRIMITIVE_XSI_TYPES = {
98
+ Primitive::CBoolean => 'C_BOOLEAN',
99
+ Primitive::CString => 'C_STRING',
100
+ Primitive::CInteger => 'C_INTEGER',
101
+ Primitive::CReal => 'C_REAL',
102
+ Primitive::CDate => 'C_DATE',
103
+ Primitive::CDateTime => 'C_DATE_TIME',
104
+ Primitive::CTime => 'C_TIME',
105
+ Primitive::CDuration => 'C_DURATION'
106
+ }.freeze
107
+
119
108
  private
120
109
 
121
- # Emits a C_OBJECT's own fields (rm_type_name/occurrence/node_id
110
+ def emit_translations(xml, translations)
111
+ return if translations.nil? || translations.empty?
112
+
113
+ xml.translations do
114
+ translations.each { |code, details| emit_translation_item(xml, code, details) }
115
+ end
116
+ end
117
+
118
+ def emit_translation_item(xml, code, details)
119
+ xml.tag!('translation', 'language' => code) do
120
+ xml.language do
121
+ xml.terminology_id { xml.value details.language.terminology_id.value }
122
+ xml.code_string details.language.code_string
123
+ end
124
+ (details.author || {}).each { |k, v| xml.author(v, 'id' => k) }
125
+ xml.accreditation details.accreditation if details.accreditation
126
+ (details.other_details || {}).each { |k, v| xml.other_details(v, 'id' => k) }
127
+ end
128
+ end
129
+
130
+ def emit_description_detail(xml, lang, item)
131
+ xml.language do
132
+ xml.terminology_id { xml.value item.language.terminology_id.value }
133
+ xml.code_string lang
134
+ end
135
+ xml.purpose item.purpose
136
+ (item.keywords || []).each { |word| xml.keywords word }
137
+ xml.use item.use if item.use
138
+ xml.misuse item.misuse if item.misuse
139
+ xml.copyright item.copyright if item.copyright
140
+ (item.original_resource_uri || {}).each { |k, v| xml.original_resource_uri(v, 'id' => k) }
141
+ (item.other_details || {}).each { |k, v| xml.other_details(v, 'id' => k) }
142
+ end
143
+
144
+ def emit_invariants
145
+ invariants = @archetype.respond_to?(:invariants) ? @archetype.invariants : nil
146
+ return '' if invariants.nil? || invariants.empty?
147
+
148
+ text = +''
149
+ xml = Builder::XmlMarkup.new(:indent => 2, :target => text)
150
+ # Each assertion is wrapped in its own <invariant> - without a
151
+ # per-item boundary, multiple invariants' tag/string_expression/
152
+ # expression triplets would flatten into indistinguishable siblings.
153
+ xml.invariants { invariants.each { |assertion| xml.tag!('invariant') { emit_assertion(xml, assertion) } } }
154
+ text
155
+ end
156
+
157
+ # Emits a C_OBJECT's own fields (rm_type_name/occurrences/node_id
122
158
  # plus, unless any_allowed?, its attributes) directly into the
123
159
  # current xml element - used both for the root <definition> and,
124
- # via emit_child, for every nested C_COMPLEX_OBJECT.
160
+ # via emit_child, for every nested C_COMPLEX_OBJECT. Matches
161
+ # OPTParser#c_archetype_root/#c_complex_object exactly.
125
162
  def emit_c_object_body(xml, node)
126
163
  xml.rm_type_name node.rm_type_name
127
- xml.occurrence { emit_interval(xml, node.occurrences) }
164
+ xml.occurrences { emit_interval(xml, node.occurrences) }
128
165
  xml.node_id node.node_id
129
166
  return if node.any_allowed?
130
167
 
@@ -136,12 +173,18 @@ module OpenEHR
136
173
  xml.upper_included interval.upper_included? unless interval.upper_included?.nil?
137
174
  xml.lower_unbounded interval.lower_unbounded?
138
175
  xml.upper_unbounded interval.upper_unbounded?
139
- xml.lower interval.lower
140
- xml.upper interval.upper
176
+ xml.lower interval.lower unless interval.lower_unbounded?
177
+ xml.upper interval.upper unless interval.upper_unbounded?
141
178
  end
142
179
 
180
+ # <attributes> itself carries the xsi:type discriminator
181
+ # (C_SINGLE_ATTRIBUTE/C_MULTIPLE_ATTRIBUTE) - OPTParser#attributes
182
+ # dispatches on exactly this attribute to pick c_single_attribute
183
+ # vs c_multiple_attribute; without it, this gem's own OPTParser
184
+ # cannot read its own XMLSerializer output back at all.
143
185
  def emit_attribute(xml, attribute)
144
- xml.attributes do
186
+ xsi_type = attribute.is_a?(CMultipleAttribute) ? 'C_MULTIPLE_ATTRIBUTE' : 'C_SINGLE_ATTRIBUTE'
187
+ xml.attributes('xsi:type' => xsi_type) do
145
188
  xml.rm_attribute_name attribute.rm_attribute_name
146
189
  xml.existence { emit_interval(xml, attribute.existence) } if attribute.existence
147
190
  emit_cardinality(xml, attribute.cardinality) if attribute.is_a?(CMultipleAttribute)
@@ -162,19 +205,32 @@ module OpenEHR
162
205
  # Dispatches an attribute's child node, tagging each <children>
163
206
  # element with an xsi:type discriminator (the AOM node's class,
164
207
  # in openEHR's own naming) since the children of one attribute
165
- # can be a mix of concrete constraint node types.
208
+ # can be a mix of concrete constraint node types. Matches
209
+ # OPTParser#children's dispatch exactly.
166
210
  def emit_child(xml, node)
167
211
  case node
168
212
  when ArchetypeSlot
169
213
  xml.children('xsi:type' => 'ARCHETYPE_SLOT') { emit_archetype_slot_body(xml, node) }
170
214
  when ArchetypeInternalRef
171
- xml.children('xsi:type' => 'ARCHETYPE_INTERNAL_REF') { xml.target_path node.target_path }
215
+ xml.children('xsi:type' => 'ARCHETYPE_INTERNAL_REF') do
216
+ xml.rm_type_name node.rm_type_name
217
+ xml.occurrences { emit_interval(xml, node.occurrences) } if node.occurrences
218
+ xml.target_path node.target_path
219
+ end
172
220
  when ConstraintRef
173
- xml.children('xsi:type' => 'CONSTRAINT_REF') { xml.reference node.reference }
221
+ xml.children('xsi:type' => 'CONSTRAINT_REF') do
222
+ xml.rm_type_name node.rm_type_name
223
+ xml.occurrences { emit_interval(xml, node.occurrences) } if node.occurrences
224
+ xml.reference node.reference
225
+ end
174
226
  when CPrimitiveObject
175
- xml.children('xsi:type' => 'C_PRIMITIVE_OBJECT') { emit_primitive_body(xml, node.item) }
227
+ xml.children('xsi:type' => 'C_PRIMITIVE_OBJECT') { emit_primitive_object_body(xml, node) }
176
228
  when OpenEHRProfile::DataTypes::Quantity::CDvOrdinal
177
229
  xml.children('xsi:type' => 'C_DV_ORDINAL') { emit_ordinal_body(xml, node) }
230
+ when OpenEHRProfile::DataTypes::Quantity::CDvScale
231
+ xml.children('xsi:type' => 'C_DV_SCALE') { emit_scale_body(xml, node) }
232
+ when OpenEHRProfile::DataTypes::Quantity::CDvQuantity
233
+ xml.children('xsi:type' => 'C_DV_QUANTITY') { emit_dv_quantity_body(xml, node) }
178
234
  when OpenEHRProfile::DataTypes::Text::CCodePhrase
179
235
  xml.children('xsi:type' => 'C_CODE_PHRASE') { emit_code_phrase_body(xml, node) }
180
236
  when CComplexObject
@@ -186,7 +242,7 @@ module OpenEHR
186
242
 
187
243
  def emit_archetype_slot_body(xml, node)
188
244
  xml.rm_type_name node.rm_type_name
189
- xml.occurrence { emit_interval(xml, node.occurrences) }
245
+ xml.occurrences { emit_interval(xml, node.occurrences) }
190
246
  xml.node_id node.node_id if node.node_id
191
247
  emit_assertions(xml, 'includes', node.includes)
192
248
  emit_assertions(xml, 'excludes', node.excludes)
@@ -195,66 +251,258 @@ module OpenEHR
195
251
  def emit_assertions(xml, tag, assertions)
196
252
  return if assertions.nil?
197
253
 
198
- xml.tag!(tag) { assertions.each { |a| xml.assertion a.string_expression } }
254
+ xml.tag!(tag) { assertions.each { |a| emit_assertion(xml, a) } }
199
255
  end
200
256
 
201
- def emit_primitive_body(xml, item)
202
- xml.primitive_type item.type
257
+ # ASSERTION: tag (optional) + string_expression (optional,
258
+ # round-trip convenience - OPTParser doesn't require it) + a real
259
+ # expression tree, matching OPTParser#assertions/#expr_leaf/
260
+ # #expr_binary_operator (rather than the previous flat
261
+ # <assertion>text</assertion>, which Assertion#expression='s
262
+ # non-nil invariant made impossible to read back at all).
263
+ def emit_assertion(xml, assertion)
264
+ xml.tag assertion.tag if assertion.tag
265
+ xml.string_expression assertion.string_expression if assertion.string_expression
266
+ emit_expression(xml, assertion.expression)
267
+ end
268
+
269
+ def emit_expression(xml, expression)
270
+ case expression
271
+ when AssertionModel::ExprBinaryOperator
272
+ xml.expression('xsi:type' => 'EXPR_BINARY_OPERATOR') { emit_binary_operator_body(xml, expression) }
273
+ when AssertionModel::ExprUnaryOperator
274
+ xml.expression('xsi:type' => 'EXPR_UNARY_OPERATOR') { emit_unary_operator_body(xml, expression) }
275
+ when AssertionModel::ExprLeaf
276
+ xml.expression('xsi:type' => 'EXPR_LEAF') { emit_expr_leaf_body(xml, expression) }
277
+ else
278
+ raise ArgumentError, "XMLSerializer cannot emit a #{expression.class} expression"
279
+ end
280
+ end
281
+
282
+ def emit_binary_operator_body(xml, expression)
283
+ xml.type expression.type
284
+ xml.operator operator_value(expression.operator)
285
+ xml.precedence_overridden expression.precedence_overridden
286
+ emit_operand(xml, 'left_operand', expression.left_operand)
287
+ emit_operand(xml, 'right_operand', expression.right_operand)
288
+ end
289
+
290
+ def emit_unary_operator_body(xml, expression)
291
+ xml.type expression.type
292
+ xml.operator operator_value(expression.operator)
293
+ xml.precedence_overridden expression.precedence_overridden
294
+ emit_operand(xml, 'operand', expression.operand)
295
+ end
296
+
297
+ # ADLParser and OPTParser disagree on how they build
298
+ # ExprOperator#operator: the ADL grammar assigns a bare
299
+ # OperatorKind::OP_* Integer constant directly, while OPTParser's
300
+ # reader wraps it in an OperatorKind object - accept either.
301
+ def operator_value(operator)
302
+ operator.respond_to?(:value) ? operator.value : operator
303
+ end
304
+
305
+ # left_operand/right_operand/operand elements carry the same
306
+ # xsi:type-tagged shape as a top-level <expression> - reuse the
307
+ # same dispatch/body emission, just under a different tag name.
308
+ def emit_operand(xml, tag, operand)
309
+ case operand
310
+ when AssertionModel::ExprBinaryOperator
311
+ xml.tag!(tag, 'xsi:type' => 'EXPR_BINARY_OPERATOR') { emit_binary_operator_body(xml, operand) }
312
+ when AssertionModel::ExprUnaryOperator
313
+ xml.tag!(tag, 'xsi:type' => 'EXPR_UNARY_OPERATOR') { emit_unary_operator_body(xml, operand) }
314
+ when AssertionModel::ExprLeaf
315
+ xml.tag!(tag, 'xsi:type' => 'EXPR_LEAF') { emit_expr_leaf_body(xml, operand) }
316
+ else
317
+ raise ArgumentError, "XMLSerializer cannot emit a #{operand.class} expression"
318
+ end
319
+ end
320
+
321
+ # OPTParser#expr_leaf dispatches on <type> (not the <item>'s own
322
+ # xsi:type) via `send type.downcase, item_leaf`: a bare "String"
323
+ # reference (a path, e.g. "archetype_id/value") reads back via
324
+ # its #string method (plain text); any C_* type reads back via
325
+ # the matching c_* primitive-item reader.
326
+ def emit_expr_leaf_body(xml, leaf)
327
+ xml.type leaf.type
328
+ if leaf.item.is_a?(Primitive::CPrimitive)
329
+ emit_primitive_item(xml, leaf.item)
330
+ else
331
+ xml.item(leaf.item.to_s, 'xsi:type' => 'xsd:string')
332
+ end
333
+ xml.reference_type leaf.reference_type
334
+ end
335
+
336
+ def emit_primitive_object_body(xml, node)
337
+ xml.rm_type_name node.rm_type_name
338
+ xml.occurrences { emit_interval(xml, node.occurrences) }
339
+ xml.node_id node.node_id if node.node_id
340
+ emit_primitive_item(xml, node.item)
341
+ end
342
+
343
+ def emit_primitive_item(xml, item)
344
+ xml.item('xsi:type' => primitive_xsi_type(item)) { emit_primitive_item_body(xml, item) }
345
+ end
346
+
347
+ def primitive_xsi_type(item)
348
+ PRIMITIVE_XSI_TYPES.fetch(item.class) { raise ArgumentError, "XMLSerializer cannot emit a #{item.class} primitive" }
349
+ end
350
+
351
+ def emit_primitive_item_body(xml, item)
203
352
  if item.is_a?(Primitive::CBoolean)
204
- xml.true_valid item.true_valid
205
- xml.false_valid item.false_valid
353
+ emit_boolean_item_body(xml, item)
354
+ elsif item.is_a?(Primitive::CDuration)
355
+ emit_duration_item_body(xml, item)
206
356
  elsif item.respond_to?(:pattern) && item.pattern
207
357
  xml.pattern item.pattern
358
+ emit_assumed_value(xml, item)
208
359
  elsif item.list
209
- item.list.each { |v| xml.item literal(v) }
360
+ item.list.each { |v| xml.list literal(v) }
361
+ emit_assumed_value(xml, item)
210
362
  elsif item.range
211
- xml.range { emit_bound_interval(xml, item.range) }
363
+ xml.range { emit_interval(xml, item.range) }
364
+ emit_assumed_value(xml, item)
365
+ else
366
+ emit_assumed_value(xml, item)
212
367
  end
213
- xml.assumed_value literal(item.assumed_value) if item.has_assumed_value?
214
368
  end
215
369
 
216
- def emit_bound_interval(xml, range)
217
- xml.lower literal(range.lower) unless range.lower_unbounded?
218
- xml.upper literal(range.upper) unless range.upper_unbounded?
370
+ def emit_boolean_item_body(xml, item)
371
+ xml.true_valid item.true_valid unless item.true_valid.nil?
372
+ xml.false_valid item.false_valid unless item.false_valid.nil?
373
+ xml.assumed_value item.assumed_value unless item.assumed_value.nil?
374
+ end
375
+
376
+ # C_DURATION's range bounds are ISO8601 duration strings (DV_
377
+ # DURATION), not plain numbers, so it can't reuse emit_interval;
378
+ # matches OPTParser#duration_range/#duration_bound exactly.
379
+ def emit_duration_item_body(xml, item)
380
+ if item.pattern
381
+ xml.pattern item.pattern
382
+ elsif item.list
383
+ item.list.each { |v| xml.list v }
384
+ elsif item.range
385
+ xml.range { emit_duration_range(xml, item.range) }
386
+ end
387
+ emit_assumed_value(xml, item)
388
+ end
389
+
390
+ def emit_duration_range(xml, range)
391
+ unless range.lower_unbounded?
392
+ xml.lower range.lower.value
393
+ xml.lower_included range.lower_included?
394
+ end
395
+ xml.lower_unbounded range.lower_unbounded?
396
+ unless range.upper_unbounded?
397
+ xml.upper range.upper.value
398
+ xml.upper_included range.upper_included?
399
+ end
400
+ xml.upper_unbounded range.upper_unbounded?
401
+ end
402
+
403
+ def emit_assumed_value(xml, item)
404
+ xml.assumed_value literal(item.assumed_value) if item.has_assumed_value?
219
405
  end
220
406
 
221
407
  def literal(value)
222
408
  value.respond_to?(:value) ? value.value.to_s : value.to_s
223
409
  end
224
410
 
411
+ # DV_ORDINAL.symbol is spec'd as DV_CODED_TEXT (code lives at
412
+ # symbol.defining_code, not symbol itself) - matches
413
+ # OPTParser#dv_ordinal_item's nested symbol/defining_code shape.
225
414
  def emit_ordinal_body(xml, node)
415
+ xml.rm_type_name node.rm_type_name
416
+ xml.occurrences { emit_interval(xml, node.occurrences) }
417
+ xml.node_id node.node_id if node.node_id
226
418
  return if node.any_allowed?
227
419
 
228
- node.list.each do |o|
229
- xml.item do
230
- xml.value o.value
231
- xml.symbol o.symbol.code_string
232
- end
420
+ node.list.each { |o| emit_ordinal_item(xml, o) }
421
+ emit_assumed_value(xml, node)
422
+ end
423
+
424
+ def emit_ordinal_item(xml, item)
425
+ xml.list do
426
+ xml.value item.value
427
+ xml.symbol { xml.defining_code { emit_code_phrase_fields(xml, item.symbol.defining_code) } }
428
+ end
429
+ end
430
+
431
+ # RM 1.1.0: same shape as C_DV_ORDINAL, but DV_SCALE.value is Real.
432
+ def emit_scale_body(xml, node)
433
+ xml.rm_type_name node.rm_type_name
434
+ xml.occurrences { emit_interval(xml, node.occurrences) }
435
+ xml.node_id node.node_id if node.node_id
436
+ return if node.any_allowed?
437
+
438
+ node.list.each { |o| emit_ordinal_item(xml, o) }
439
+ end
440
+
441
+ def emit_dv_quantity_body(xml, node)
442
+ xml.rm_type_name node.rm_type_name
443
+ xml.occurrences { emit_interval(xml, node.occurrences) }
444
+ xml.node_id node.node_id if node.node_id
445
+ return if node.any_allowed?
446
+
447
+ xml.property { emit_code_phrase_fields(xml, node.property) } if node.property
448
+ (node.list || []).each { |item| emit_quantity_item(xml, item) }
449
+ emit_dv_quantity_assumed_value(xml, node.assumed_value) if node.assumed_value
450
+ end
451
+
452
+ def emit_quantity_item(xml, item)
453
+ xml.list do
454
+ xml.units item.units
455
+ xml.magnitude { emit_interval(xml, item.magnitude) } if item.magnitude
456
+ xml.precision { emit_interval(xml, item.precision) } unless item.precision_unconstrained?
457
+ end
458
+ end
459
+
460
+ # assumed_value here is a real DV_QUANTITY (plain magnitude/
461
+ # precision, not a range) - distinct from CQuantityItem's ranged
462
+ # list items above.
463
+ def emit_dv_quantity_assumed_value(xml, assumed_value)
464
+ xml.assumed_value do
465
+ xml.units assumed_value.units
466
+ xml.magnitude assumed_value.magnitude
467
+ xml.precision assumed_value.precision unless assumed_value.precision.nil?
233
468
  end
234
- xml.assumed_value literal(node.assumed_value) if node.has_assumed_value?
235
469
  end
236
470
 
237
471
  def emit_code_phrase_body(xml, node)
472
+ xml.rm_type_name node.rm_type_name
473
+ xml.occurrences { emit_interval(xml, node.occurrences) }
474
+ xml.node_id node.node_id if node.node_id
238
475
  return if node.any_allowed?
239
476
 
240
- xml.terminology_id node.terminology_id.value
241
- node.code_list.each { |c| xml.code c }
477
+ xml.terminology_id { xml.value node.terminology_id.value }
478
+ node.code_list.each { |c| xml.code_list c }
242
479
  end
243
480
 
244
- def emit_term_bindings(xml, term_bindings)
245
- return if term_bindings.nil?
481
+ def emit_code_phrase_fields(xml, code_phrase)
482
+ xml.terminology_id { xml.value code_phrase.terminology_id.value }
483
+ xml.code_string code_phrase.code_string
484
+ end
246
485
 
247
- term_bindings.each do |terminology, codes|
248
- codes.each do |code, bindings|
249
- code_phrase = Array(bindings).first
250
- xml.term_bindings do
251
- xml.terminology terminology
252
- xml.code code
253
- xml.value "#{code_phrase.terminology_id.value}::#{code_phrase.code_string}"
486
+ def emit_term_definitions(xml, keyword, term_definitions)
487
+ term_definitions.each do |lang, terms|
488
+ terms.each do |code, term|
489
+ xml.tag!(keyword, 'language' => lang, 'code' => code) do
490
+ term.items.each { |key, value| xml.items(value, 'id' => key) }
254
491
  end
255
492
  end
256
493
  end
257
494
  end
495
+
496
+ def emit_bindings(xml, keyword, bindings)
497
+ return if bindings.nil?
498
+
499
+ bindings.each do |terminology, codes|
500
+ codes.each do |code, binding|
501
+ value_object = Array(binding).first
502
+ xml.tag!(keyword, yield(value_object), 'terminology' => terminology, 'code' => code)
503
+ end
504
+ end
505
+ end
258
506
  end
259
507
  end
260
508
  end
@@ -1,3 +1,3 @@
1
1
  module OpenEHR
2
- VERSION = "2.0.2"
2
+ VERSION = "2.3.0"
3
3
  end