openehr 2.1.0 → 2.3.1

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.
@@ -7,14 +7,14 @@ module OpenEHR
7
7
  class ADLSerializer < BaseSerializer
8
8
  def header
9
9
  hd = +'archetype'
10
- unless @archetype.adl_version.nil?
11
- hd << " (adl_version = #{@archetype.adl_version})"
12
- end
13
- hd << (NL+INDENT + "#{@archetype.archetype_id.value}"+(NL*2))
14
- hd << ('concept'+NL+ INDENT+"[#{@archetype.concept}]"+NL)
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)
15
14
  hd << (NL+'language'+NL+INDENT+'original_language = <['+
16
15
  @archetype.original_language.terminology_id.value+'::'+
17
16
  @archetype.original_language.code_string+']>'+NL)
17
+ hd << translations_block(@archetype.translations)
18
18
  return hd
19
19
  end
20
20
 
@@ -45,15 +45,18 @@ module OpenEHR
45
45
  def ontology
46
46
  ao = @archetype.ontology
47
47
  ontology = 'ontology'+NL
48
+ ontology << primary_language_line(ao.primary_language)
48
49
  ontology << string_list_line('languages_available', ao.languages_available)
49
50
  ontology << string_list_line('terminologies_available', ao.terminologies_available)
50
51
  ontology << term_definitions_block(ao.term_definitions)
52
+ ontology << term_definitions_block(ao.constraint_definitions, 'constraint_definitions') if ao.constraint_definitions
51
53
  ontology << term_bindings_block(ao.term_bindings)
54
+ ontology << constraint_bindings_block(ao.constraint_bindings)
52
55
  ontology
53
56
  end
54
57
 
55
58
  def merge
56
- return header + NL + description + NL + definition + NL + ontology
59
+ return header + NL + description + NL + definition + NL + invariant_section + ontology
57
60
  end
58
61
 
59
62
  include OpenEHR::AM::Archetype::ConstraintModel
@@ -68,6 +71,56 @@ module OpenEHR
68
71
  INDENT + "#{keyword} = <" + values.map { |v| "\"#{v}\"" }.join(', ') + '>' + NL
69
72
  end
70
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
+
71
124
  def description_details_item(lang, item)
72
125
  block = (INDENT*2)+'["'+lang+'"] = <'+NL
73
126
  block << description_language_line(item.language)
@@ -109,8 +162,8 @@ module OpenEHR
109
162
  block << ((INDENT*3)+'>'+NL)
110
163
  end
111
164
 
112
- def term_definitions_block(term_definitions)
113
- block = INDENT + 'term_definitions = <' + NL
165
+ def term_definitions_block(term_definitions, keyword = 'term_definitions')
166
+ block = INDENT + "#{keyword} = <" + NL
114
167
  term_definitions.each do |lang, items|
115
168
  block << ((INDENT*2) + "[\"#{lang}\"] = <" + NL)
116
169
  block << ((INDENT*3) + 'items = <' + NL)
@@ -144,9 +197,28 @@ module OpenEHR
144
197
  block << (INDENT + '>' + NL)
145
198
  end
146
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)
217
+ end
218
+
147
219
  # Recursive cADL emitter for a single C_OBJECT node, dispatching
148
- # on its concrete class. Unsupported domain types (e.g.
149
- # 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
150
222
  # raise rather than emit a guess at their syntax.
151
223
  def emit_c_object(node, depth)
152
224
  case node
@@ -158,6 +230,14 @@ module OpenEHR
158
230
  emit_constraint_ref(node, depth)
159
231
  when CComplexObject
160
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'
161
241
  else
162
242
  raise ArgumentError, "ADLSerializer cannot emit a #{node.class} node"
163
243
  end
@@ -192,7 +272,7 @@ module OpenEHR
192
272
  head = attribute_head(attribute, depth)
193
273
  children = attribute.children || []
194
274
  if children.size == 1 && inline?(children.first)
195
- head + " matches {#{inline_body(children.first)}}" + NL
275
+ head + " matches {#{inline_body(children.first, depth)}}" + NL
196
276
  else
197
277
  body = children.map { |c| emit_c_object(c, depth + 1) }.join
198
278
  head + ' matches {' + NL + body + (INDENT*depth) + '}' + NL
@@ -209,10 +289,11 @@ module OpenEHR
209
289
  def inline?(node)
210
290
  node.is_a?(CPrimitiveObject) || node.is_a?(ConstraintRef) ||
211
291
  node.is_a?(OpenEHRProfile::DataTypes::Quantity::CDvOrdinal) ||
212
- node.is_a?(OpenEHRProfile::DataTypes::Text::CCodePhrase)
292
+ node.is_a?(OpenEHRProfile::DataTypes::Text::CCodePhrase) ||
293
+ node.is_a?(OpenEHRProfile::DataTypes::Quantity::CDvQuantity)
213
294
  end
214
295
 
215
- def inline_body(node)
296
+ def inline_body(node, depth)
216
297
  case node
217
298
  when CPrimitiveObject
218
299
  primitive_body(node.item)
@@ -222,9 +303,56 @@ module OpenEHR
222
303
  ordinal_body(node)
223
304
  when OpenEHRProfile::DataTypes::Text::CCodePhrase
224
305
  code_phrase_body(node)
306
+ when OpenEHRProfile::DataTypes::Quantity::CDvQuantity
307
+ cdv_quantity_body(node, depth)
225
308
  end
226
309
  end
227
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
+
228
356
  def emit_archetype_slot(node, depth)
229
357
  head = (INDENT*depth) + 'allow_archetype ' + node.rm_type_name
230
358
  head += "[#{node.node_id}]" if node.node_id
@@ -319,10 +447,19 @@ module OpenEHR
319
447
  def ordinal_body(node)
320
448
  return 'C_DV_ORDINAL < >' if node.any_allowed?
321
449
 
322
- 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(', ')
323
451
  with_assumed_value(body, node)
324
452
  end
325
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
+
326
463
  def code_phrase_body(node)
327
464
  return '*' if node.any_allowed?
328
465