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,66 +1,39 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'base'
2
4
 
3
5
  module OpenEHR
4
6
  module Serializer
5
7
  class ADLSerializer < BaseSerializer
6
8
  def header
7
- hd = 'archetype'
8
- unless @archetype.adl_version.nil?
9
- hd << " (adl_version = #{@archetype.adl_version})"
10
- end
11
- hd << NL+INDENT + "#{@archetype.archetype_id.value}"+NL*2
12
- hd << 'concept'+NL+ INDENT+"[#{@archetype.concept}]"+NL
13
- hd << NL+'language'+NL+INDENT+'original_language = <['+
9
+ hd = +'archetype'
10
+ hd << archetype_meta_data_clause
11
+ hd << (NL+INDENT + "#{@archetype.archetype_id.value}"+NL)
12
+ hd << specialise_section
13
+ hd << (NL+'concept'+NL+ INDENT+"[#{@archetype.concept}]"+NL)
14
+ hd << (NL+'language'+NL+INDENT+'original_language = <['+
14
15
  @archetype.original_language.terminology_id.value+'::'+
15
- @archetype.original_language.code_string+']>'+NL
16
+ @archetype.original_language.code_string+']>'+NL)
17
+ hd << translations_block(@archetype.translations)
16
18
  return hd
17
19
  end
18
20
 
19
21
  def description
20
- desc = ''
22
+ desc = +''
21
23
  if @archetype.description
22
24
  ad = @archetype.description
23
- desc << 'description' + NL
24
- desc << INDENT + 'original_author = <' + NL
25
+ desc << ('description' + NL)
26
+ desc << (INDENT + 'original_author = <' + NL)
25
27
  ad.original_author.each do |k,v|
26
- desc << INDENT+INDENT+'["'+k+'"] = <"'+v+'">'+NL
28
+ desc << (INDENT+INDENT+'["'+k+'"] = <"'+v+'">'+NL)
27
29
  end
28
- desc << INDENT+'>'+NL
29
- desc << INDENT+'lifecycle_state = <"'+ad.lifecycle_state+'">'+NL
30
- desc << INDENT+'details = <'+NL
30
+ desc << (INDENT+'>'+NL)
31
+ desc << (INDENT+'lifecycle_state = <"'+ad.lifecycle_state+'">'+NL)
32
+ desc << (INDENT+'details = <'+NL)
31
33
  ad.details.each do |lang,item|
32
- desc << INDENT*2+'["'+lang+'"] = <'+NL
33
- desc << INDENT*3+'language = <['+
34
- item.language.terminology_id.value+'::'+
35
- item.language.code_string+']>'+NL
36
- desc << INDENT*3+'purpose = <"'+item.purpose+'">'+NL
37
- if item.keywords then
38
- desc << INDENT*3+'keywords = <'
39
- item.keywords.each do |word|
40
- desc << '"'+word+'",'
41
- end
42
- desc.chop! << '>'+NL
43
- end
44
- desc << INDENT*3+'use = <"'+item.use+'">'+NL if item.use
45
- desc << INDENT*3+'misuse = <"'+item.misuse+'">'+NL if item.misuse
46
- desc << INDENT*3+'copyright = <"'+item.copyright+'">'+NL if item.copyright
47
- if item.original_resource_uri
48
- desc << INDENT*3 + 'original_resource_uri = <'
49
- item.original_resource_uri.each do |k,v|
50
- desc << INDENT*4+'["'+k+'"] = <"'+v+'">'+NL
51
- end
52
- desc << INDENT*3+'>'+NL
53
- end
54
- if item.other_details
55
- desc << INDENT*3 + 'other_details = <'
56
- item.other_details.each do |k,v|
57
- desc << INDENT*4+'["'+k+'"] = <"'+v+'">'+NL
58
- end
59
- desc << INDENT*3+'>'+NL
60
- end
61
- desc << INDENT*2+'>'+NL
34
+ desc << description_details_item(lang, item)
62
35
  end
63
- desc << INDENT+'>'+NL
36
+ desc << (INDENT+'>'+NL)
64
37
  end
65
38
  return desc
66
39
  end
@@ -72,29 +45,18 @@ module OpenEHR
72
45
  def ontology
73
46
  ao = @archetype.ontology
74
47
  ontology = 'ontology'+NL
48
+ ontology << primary_language_line(ao.primary_language)
75
49
  ontology << string_list_line('languages_available', ao.languages_available)
76
50
  ontology << string_list_line('terminologies_available', ao.terminologies_available)
77
- ontology << INDENT + 'term_definitions = <' + NL
78
- ao.term_definitions.each do |lang, items|
79
- ontology << INDENT*2 + "[\"#{lang}\"] = <" + NL
80
- ontology << INDENT*3 + 'items = <' + NL
81
- items.each do |code, item|
82
- ontology << INDENT*4 + "[\"#{code}\"] = <" + NL
83
- item.items.each do |name, desc|
84
- ontology << INDENT*5 + "#{name} = <\"#{desc}\">" + NL
85
- end
86
- ontology << INDENT*4 + '>'+NL
87
- end
88
- ontology << INDENT*3 + '>' + NL
89
- ontology << INDENT*2 + '>' + NL
90
- end
91
- ontology << INDENT + '>' + NL
51
+ ontology << term_definitions_block(ao.term_definitions)
52
+ ontology << term_definitions_block(ao.constraint_definitions, 'constraint_definitions') if ao.constraint_definitions
92
53
  ontology << term_bindings_block(ao.term_bindings)
54
+ ontology << constraint_bindings_block(ao.constraint_bindings)
93
55
  ontology
94
56
  end
95
57
 
96
58
  def merge
97
- return header + NL + description + NL + definition + NL + ontology
59
+ return header + NL + description + NL + definition + NL + invariant_section + ontology
98
60
  end
99
61
 
100
62
  include OpenEHR::AM::Archetype::ConstraintModel
@@ -109,26 +71,154 @@ module OpenEHR
109
71
  INDENT + "#{keyword} = <" + values.map { |v| "\"#{v}\"" }.join(', ') + '>' + NL
110
72
  end
111
73
 
74
+ # archetype (adl_version = ...; uid = ...) - a ';'-separated
75
+ # metadata clause. Empty when neither is present, so callers that
76
+ # never set uid see byte-identical output to before uid support.
77
+ def archetype_meta_data_clause
78
+ items = []
79
+ items << "adl_version = #{@archetype.adl_version}" if @archetype.adl_version
80
+ items << "uid = #{@archetype.uid.value}" if @archetype.uid
81
+ return '' if items.empty?
82
+
83
+ " (#{items.join('; ')})"
84
+ end
85
+
86
+ def specialise_section
87
+ return '' if @archetype.parent_archetype_id.nil?
88
+
89
+ 'specialise' + NL + INDENT + @archetype.parent_archetype_id.value + NL
90
+ end
91
+
92
+ def translations_block(translations)
93
+ return '' if translations.nil? || translations.empty?
94
+
95
+ block = INDENT + 'translations = <' + NL
96
+ translations.each { |code, details| block << translation_item_block(code, details) }
97
+ block << (INDENT + '>' + NL)
98
+ end
99
+
100
+ def translation_item_block(code, details)
101
+ block = (INDENT*2) + "[\"#{code}\"] = <" + NL
102
+ block << ((INDENT*3) + "language = <[#{details.language.terminology_id.value}::#{details.language.code_string}]>" + NL)
103
+ block << keyed_map_block('author', details.author) if details.author
104
+ block << ((INDENT*3) + "accreditation = <\"#{details.accreditation}\">" + NL) if details.accreditation
105
+ block << keyed_map_block('other_details', details.other_details) if details.other_details
106
+ block << ((INDENT*2) + '>' + NL)
107
+ end
108
+
109
+ def invariant_section
110
+ invariants = @archetype.invariants
111
+ return '' if invariants.nil? || invariants.empty?
112
+
113
+ block = 'invariant' + NL
114
+ invariants.each { |assertion| block << (INDENT + assertion.string_expression + NL) }
115
+ block << NL
116
+ end
117
+
118
+ def primary_language_line(primary_language)
119
+ return '' if primary_language.nil?
120
+
121
+ INDENT + "primary_language = <\"#{primary_language}\">" + NL
122
+ end
123
+
124
+ def description_details_item(lang, item)
125
+ block = (INDENT*2)+'["'+lang+'"] = <'+NL
126
+ block << description_language_line(item.language)
127
+ block << ((INDENT*3)+'purpose = <"'+item.purpose+'">'+NL)
128
+ block << description_keywords_line(item) if item.keywords
129
+ block << optional_field_line('use', item.use)
130
+ block << optional_field_line('misuse', item.misuse)
131
+ block << optional_field_line('copyright', item.copyright)
132
+ block << keyed_map_block('original_resource_uri', item.original_resource_uri) if item.original_resource_uri
133
+ block << keyed_map_block('other_details', item.other_details) if item.other_details
134
+ block << ((INDENT*2)+'>'+NL)
135
+ end
136
+
137
+ def description_language_line(language)
138
+ (INDENT*3)+'language = <['+
139
+ language.terminology_id.value+'::'+
140
+ language.code_string+']>'+NL
141
+ end
142
+
143
+ def optional_field_line(keyword, value)
144
+ return '' if value.nil?
145
+
146
+ (INDENT*3)+"#{keyword} = <\"#{value}\">"+NL
147
+ end
148
+
149
+ def description_keywords_line(item)
150
+ block = (INDENT*3)+'keywords = <'
151
+ item.keywords.each do |word|
152
+ block << ('"'+word+'",')
153
+ end
154
+ block.chop! << ('>'+NL)
155
+ end
156
+
157
+ def keyed_map_block(keyword, hash)
158
+ block = (INDENT*3) + "#{keyword} = <"
159
+ hash.each do |k,v|
160
+ block << ((INDENT*4)+'["'+k+'"] = <"'+v+'">'+NL)
161
+ end
162
+ block << ((INDENT*3)+'>'+NL)
163
+ end
164
+
165
+ def term_definitions_block(term_definitions, keyword = 'term_definitions')
166
+ block = INDENT + "#{keyword} = <" + NL
167
+ term_definitions.each do |lang, items|
168
+ block << ((INDENT*2) + "[\"#{lang}\"] = <" + NL)
169
+ block << ((INDENT*3) + 'items = <' + NL)
170
+ items.each do |code, item|
171
+ block << ((INDENT*4) + "[\"#{code}\"] = <" + NL)
172
+ item.items.each do |name, desc|
173
+ block << ((INDENT*5) + "#{name} = <\"#{desc}\">" + NL)
174
+ end
175
+ block << ((INDENT*4) + '>'+NL)
176
+ end
177
+ block << ((INDENT*3) + '>' + NL)
178
+ block << ((INDENT*2) + '>' + NL)
179
+ end
180
+ block << (INDENT + '>' + NL)
181
+ end
182
+
112
183
  def term_bindings_block(term_bindings)
113
184
  return '' if term_bindings.nil? || term_bindings.empty?
114
185
 
115
186
  block = INDENT + 'term_bindings = <' + NL
116
187
  term_bindings.each do |terminology, codes|
117
- block << INDENT*2 + "[\"#{terminology}\"] = <" + NL
118
- block << INDENT*3 + 'items = <' + NL
188
+ block << ((INDENT*2) + "[\"#{terminology}\"] = <" + NL)
189
+ block << ((INDENT*3) + 'items = <' + NL)
119
190
  codes.each do |code, bindings|
120
191
  code_phrase = Array(bindings).first
121
- block << INDENT*4 + "[\"#{code}\"] = <[#{code_phrase.terminology_id.value}::#{code_phrase.code_string}]>" + NL
192
+ block << ((INDENT*4) + "[\"#{code}\"] = <[#{code_phrase.terminology_id.value}::#{code_phrase.code_string}]>" + NL)
122
193
  end
123
- block << INDENT*3 + '>' + NL
124
- block << INDENT*2 + '>' + NL
194
+ block << ((INDENT*3) + '>' + NL)
195
+ block << ((INDENT*2) + '>' + NL)
125
196
  end
126
- block << INDENT + '>' + NL
197
+ block << (INDENT + '>' + NL)
198
+ end
199
+
200
+ # constraint_bindings mirrors term_bindings' 3-level nesting
201
+ # (terminology -> items -> code), but each value is a bare/unquoted
202
+ # URI (a DvUri) rather than a qualified term code reference.
203
+ def constraint_bindings_block(constraint_bindings)
204
+ return '' if constraint_bindings.nil? || constraint_bindings.empty?
205
+
206
+ block = INDENT + 'constraint_bindings = <' + NL
207
+ constraint_bindings.each do |terminology, codes|
208
+ block << ((INDENT*2) + "[\"#{terminology}\"] = <" + NL)
209
+ block << ((INDENT*3) + 'items = <' + NL)
210
+ codes.each do |code, uri|
211
+ block << ((INDENT*4) + "[\"#{code}\"] = <#{uri.value}>" + NL)
212
+ end
213
+ block << ((INDENT*3) + '>' + NL)
214
+ block << ((INDENT*2) + '>' + NL)
215
+ end
216
+ block << (INDENT + '>' + NL)
127
217
  end
128
218
 
129
219
  # Recursive cADL emitter for a single C_OBJECT node, dispatching
130
- # on its concrete class. Unsupported domain types (e.g.
131
- # C_DV_QUANTITY's dADL block syntax, C_DV_STATE) deliberately
220
+ # on its concrete class. Domain types with no ADL 1.4 grammar rule
221
+ # (C_DV_SCALE, C_DV_STATE - both RM 1.1.0+ additions) deliberately
132
222
  # raise rather than emit a guess at their syntax.
133
223
  def emit_c_object(node, depth)
134
224
  case node
@@ -140,6 +230,14 @@ module OpenEHR
140
230
  emit_constraint_ref(node, depth)
141
231
  when CComplexObject
142
232
  emit_complex_object(node, depth)
233
+ when OpenEHRProfile::DataTypes::Quantity::CDvScale
234
+ raise ArgumentError,
235
+ 'ADLSerializer cannot emit C_DV_SCALE in cADL - the ADL 1.4 grammar has no C_DV_SCALE rule ' \
236
+ '(RM 1.1.0 addition); use the XML serializer instead'
237
+ when OpenEHRProfile::DataTypes::Basic::CDvState
238
+ raise ArgumentError,
239
+ 'ADLSerializer cannot emit C_DV_STATE in cADL - the ADL 1.4 grammar has no C_DV_STATE rule; ' \
240
+ 'use the XML serializer instead'
143
241
  else
144
242
  raise ArgumentError, "ADLSerializer cannot emit a #{node.class} node"
145
243
  end
@@ -151,12 +249,12 @@ module OpenEHR
151
249
  head + ' matches {*}' + NL
152
250
  else
153
251
  body = node.attributes.map { |a| emit_c_attribute(a, depth + 1) }.join
154
- head + ' matches {' + NL + body + INDENT*depth + '}' + NL
252
+ head + ' matches {' + NL + body + (INDENT*depth) + '}' + NL
155
253
  end
156
254
  end
157
255
 
158
256
  def complex_object_head(node, depth)
159
- head = INDENT*depth + node.rm_type_name
257
+ head = (INDENT*depth) + node.rm_type_name
160
258
  head += "[#{node.node_id}]" if node.node_id
161
259
  head += " occurrences matches {#{interval_literal(node.occurrences)}}" if show_interval?(node.occurrences)
162
260
  head
@@ -174,15 +272,15 @@ module OpenEHR
174
272
  head = attribute_head(attribute, depth)
175
273
  children = attribute.children || []
176
274
  if children.size == 1 && inline?(children.first)
177
- head + " matches {#{inline_body(children.first)}}" + NL
275
+ head + " matches {#{inline_body(children.first, depth)}}" + NL
178
276
  else
179
277
  body = children.map { |c| emit_c_object(c, depth + 1) }.join
180
- head + ' matches {' + NL + body + INDENT*depth + '}' + NL
278
+ head + ' matches {' + NL + body + (INDENT*depth) + '}' + NL
181
279
  end
182
280
  end
183
281
 
184
282
  def attribute_head(attribute, depth)
185
- head = INDENT*depth + attribute.rm_attribute_name
283
+ head = (INDENT*depth) + attribute.rm_attribute_name
186
284
  head += " existence matches {#{interval_literal(attribute.existence)}}" if show_interval?(attribute.existence)
187
285
  head += " cardinality matches {#{cardinality_literal(attribute.cardinality)}}" if attribute.is_a?(CMultipleAttribute)
188
286
  head
@@ -191,10 +289,11 @@ module OpenEHR
191
289
  def inline?(node)
192
290
  node.is_a?(CPrimitiveObject) || node.is_a?(ConstraintRef) ||
193
291
  node.is_a?(OpenEHRProfile::DataTypes::Quantity::CDvOrdinal) ||
194
- node.is_a?(OpenEHRProfile::DataTypes::Text::CCodePhrase)
292
+ node.is_a?(OpenEHRProfile::DataTypes::Text::CCodePhrase) ||
293
+ node.is_a?(OpenEHRProfile::DataTypes::Quantity::CDvQuantity)
195
294
  end
196
295
 
197
- def inline_body(node)
296
+ def inline_body(node, depth)
198
297
  case node
199
298
  when CPrimitiveObject
200
299
  primitive_body(node.item)
@@ -204,34 +303,81 @@ module OpenEHR
204
303
  ordinal_body(node)
205
304
  when OpenEHRProfile::DataTypes::Text::CCodePhrase
206
305
  code_phrase_body(node)
306
+ when OpenEHRProfile::DataTypes::Quantity::CDvQuantity
307
+ cdv_quantity_body(node, depth)
207
308
  end
208
309
  end
209
310
 
311
+ # cADL syntax for C_DV_QUANTITY is a dADL-style block, not a short
312
+ # inline literal like the other inline? types above - see the
313
+ # adl_grammar.tt c_dv_quantity rule and CDvQuantityItems#value
314
+ # (adl_helper.rb) for the exact shape this mirrors: property is a
315
+ # qualified term code reference, list is an array of CQuantityItem
316
+ # keyed by 1-based position (the key itself isn't stored on
317
+ # CQuantityItem, so round-tripping only needs *a* stable key, not
318
+ # the original one), and assumed_value is a real DV_QUANTITY (plain
319
+ # magnitude/precision, not a range).
320
+ def cdv_quantity_body(node, depth)
321
+ return 'C_DV_QUANTITY < >' if node.any_allowed?
322
+
323
+ body = 'C_DV_QUANTITY <' + NL
324
+ body << quantity_property_line(node.property, depth + 1) if node.property
325
+ body << quantity_list_block(node.list, depth + 1) if node.list
326
+ body << assumed_quantity_block(node.assumed_value, depth + 1) if node.assumed_value
327
+ body << ((INDENT*depth) + '>')
328
+ end
329
+
330
+ def quantity_property_line(property, depth)
331
+ (INDENT*depth) + "property = <[#{property.terminology_id.value}::#{property.code_string}]>" + NL
332
+ end
333
+
334
+ def quantity_list_block(list, depth)
335
+ block = (INDENT*depth) + 'list = <' + NL
336
+ list.each_with_index { |item, index| block << quantity_item_block(item, index + 1, depth + 1) }
337
+ block << ((INDENT*depth) + '>' + NL)
338
+ end
339
+
340
+ def quantity_item_block(item, key, depth)
341
+ block = (INDENT*depth) + "[\"#{key}\"] = <" + NL
342
+ block << ((INDENT*(depth+1)) + "units = <\"#{item.units}\">" + NL)
343
+ block << ((INDENT*(depth+1)) + "magnitude = <|#{range_literal(item.magnitude)}|>" + NL) if item.magnitude
344
+ block << ((INDENT*(depth+1)) + "precision = <|#{range_literal(item.precision)}|>" + NL) if item.precision
345
+ block << ((INDENT*depth) + '>' + NL)
346
+ end
347
+
348
+ def assumed_quantity_block(assumed_value, depth)
349
+ block = (INDENT*depth) + 'assumed_value = <' + NL
350
+ block << ((INDENT*(depth+1)) + "units = <\"#{assumed_value.units}\">" + NL)
351
+ block << ((INDENT*(depth+1)) + "magnitude = <#{assumed_value.magnitude}>" + NL)
352
+ block << ((INDENT*(depth+1)) + "precision = <#{assumed_value.precision}>" + NL) unless assumed_value.precision.nil?
353
+ block << ((INDENT*depth) + '>' + NL)
354
+ end
355
+
210
356
  def emit_archetype_slot(node, depth)
211
- head = INDENT*depth + 'allow_archetype ' + node.rm_type_name
357
+ head = (INDENT*depth) + 'allow_archetype ' + node.rm_type_name
212
358
  head += "[#{node.node_id}]" if node.node_id
213
359
  head += " occurrences matches {#{interval_literal(node.occurrences)}}" if show_interval?(node.occurrences)
214
360
  lines = [head + ' matches {' + NL]
215
361
  lines << assertion_block(node.includes, 'include', depth + 1)
216
362
  lines << assertion_block(node.excludes, 'exclude', depth + 1)
217
- lines << INDENT*depth + '}' + NL
363
+ lines << ((INDENT*depth) + '}' + NL)
218
364
  lines.join
219
365
  end
220
366
 
221
367
  def assertion_block(assertions, keyword, depth)
222
368
  return '' if assertions.nil?
223
369
 
224
- lines = [INDENT*depth + keyword + NL]
225
- assertions.each { |a| lines << INDENT*(depth+1) + a.string_expression + NL }
370
+ lines = [(INDENT*depth) + keyword + NL]
371
+ assertions.each { |a| lines << ((INDENT*(depth+1)) + a.string_expression + NL) }
226
372
  lines.join
227
373
  end
228
374
 
229
375
  def emit_internal_ref(node, depth)
230
- INDENT*depth + "use_node #{node.rm_type_name} #{node.target_path}" + NL
376
+ (INDENT*depth) + "use_node #{node.rm_type_name} #{node.target_path}" + NL
231
377
  end
232
378
 
233
379
  def emit_constraint_ref(node, depth)
234
- INDENT*depth + "[#{node.reference}]" + NL
380
+ (INDENT*depth) + "[#{node.reference}]" + NL
235
381
  end
236
382
 
237
383
  def primitive_body(item)
@@ -301,10 +447,19 @@ module OpenEHR
301
447
  def ordinal_body(node)
302
448
  return 'C_DV_ORDINAL < >' if node.any_allowed?
303
449
 
304
- body = node.list.map { |o| "#{o.value}|[#{o.symbol.code_string}]" }.join(', ')
450
+ body = node.list.map { |o| "#{o.value}|[#{ordinal_symbol_reference(o.symbol)}]" }.join(', ')
305
451
  with_assumed_value(body, node)
306
452
  end
307
453
 
454
+ # The c_ordinal grammar rule builds symbol as a DV_CODED_TEXT
455
+ # (code lives at symbol.defining_code, not symbol itself) - a bare
456
+ # CODE_PHRASE-like symbol is also accepted, matching the dual
457
+ # shape CDvOrdinal#valid_value? already tolerates.
458
+ def ordinal_symbol_reference(symbol)
459
+ code_phrase = symbol.respond_to?(:defining_code) ? symbol.defining_code : symbol
460
+ "#{code_phrase.terminology_id.value}::#{code_phrase.code_string}"
461
+ end
462
+
308
463
  def code_phrase_body(node)
309
464
  return '*' if node.any_allowed?
310
465
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OpenEHR
2
4
  module Serializer
3
5
  NL = "\r\n"
@@ -1,5 +1,4 @@
1
1
  require 'json'
2
- require 'set'
3
2
 
4
3
  module OpenEHR
5
4
  module Serializer