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.
@@ -1,12 +1,19 @@
1
1
  require 'nokogiri'
2
+ require_relative 'xml_constraint_parsing'
3
+ require_relative 'xml_primitive_parsing'
4
+ require_relative 'xml_domain_type_parsing'
2
5
 
3
6
  module OpenEHR
4
7
  module Parser
5
8
  class OPTParser < ::OpenEHR::Parser::Base
9
+ include XMLConstraintParsing
10
+ include XMLPrimitiveParsing
11
+ include XMLDomainTypeParsing
12
+
6
13
  TEMPLATE_LANGUAGE_CODE_PATH =
7
14
  '/template/language/code_string'
8
15
  TEMPLATE_LANGUAGE_TERM_ID_PATH =
9
- '/template/language/terminology_id/value'
16
+ '/template/language/terminology_id/value'
10
17
  TEMPLATE_ID_PATH = '/template/template_id/value'
11
18
  UID_PATH = '/template/uid/value'
12
19
  CONCEPT_PATH = '/template/concept'
@@ -35,10 +42,10 @@ module OpenEHR
35
42
  def parse
36
43
  @opt = Nokogiri::XML::Document.parse(File.open(@filename))
37
44
  @opt.remove_namespaces!
38
-
45
+
39
46
  uid = build_uid
40
47
  defs = definition
41
-
48
+
42
49
  # Create operational template with archetype-compatible parameters
43
50
  OpenEHR::AM::Template::OperationalTemplate.new(
44
51
  uid: uid,
@@ -119,7 +126,7 @@ module OpenEHR
119
126
  # Create a basic ontology for the template using the main concept
120
127
  concept_code = 'at0000'
121
128
  original_lang = language
122
-
129
+
123
130
  term_definitions = {
124
131
  original_lang.code_string => [
125
132
  OpenEHR::AM::Archetype::Terminology::ArchetypeTerm.new(
@@ -131,7 +138,7 @@ module OpenEHR
131
138
  )
132
139
  ]
133
140
  }
134
-
141
+
135
142
  OpenEHR::AM::Archetype::Terminology::ArchetypeTerminology.new(
136
143
  concept_code: concept_code,
137
144
  original_language: original_lang,
@@ -160,418 +167,6 @@ module OpenEHR
160
167
  { language.code_string => term_items }
161
168
  end
162
169
 
163
- def c_archetype_root(xml, node = Node.new)
164
- rm_type_name = text_on_path(xml, './rm_type_name')
165
- id = text_on_path(xml, './node_id')
166
- node.id = id unless id.nil? or id.empty?
167
- occurrences = occurrences(xml.xpath('./occurrences'))
168
- archetype_id = OpenEHR::RM::Support::Identification::ArchetypeID.new(value: text_on_path(xml, './archetype_id/value'))
169
- if node.root? or node.id.nil?
170
- node.path = "/"
171
- # else
172
- # node.path += "/" #"/[#{archetype_id.value}]"
173
- end
174
- component_terminologies(archetype_id, xml)
175
- 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))
176
- end
177
-
178
- def c_complex_object(xml, node)
179
- rm_type_name = xml.xpath('./rm_type_name').text
180
- node_id = xml.xpath('./node_id').text
181
- unless node_id.nil? or node_id.empty?
182
- node.id = node_id
183
- node.path = "#{node.path}[#{node.id}]"
184
- end
185
- 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))
186
- end
187
-
188
- def attributes(attributes_xml, node)
189
- attributes_xml.map do |attr|
190
- rm_attribute_name = attr.at('rm_attribute_name').text
191
- if node.root?
192
- path = "/#{rm_attribute_name}"
193
- # elsif node.id
194
- # path = "#{node.path}[#{node.id}]/#{rm_attribute_name}"
195
- else
196
- path = "#{node.path}/#{rm_attribute_name}"
197
- end
198
- child_node = Node.new(node)
199
- child_node.path = path
200
- child_node.id = node.id
201
- send attr.attributes['type'].text.downcase, attr, child_node
202
- end
203
- end
204
-
205
- def children(children_xml, node)
206
- children_xml.map do |child|
207
- send child.attributes['type'].text.downcase, child, node
208
- end
209
- end
210
-
211
- def c_single_attribute(attr_xml, node)
212
- rm_attribute_name = attr_xml.at('rm_attribute_name').text
213
- existence = occurrences(attr_xml.at('existence'))
214
- OpenEHR::AM::Archetype::ConstraintModel::CSingleAttribute.new(rm_attribute_name: rm_attribute_name, existence: existence, path: node.path, children: children(attr_xml.xpath('./children'), node))
215
- end
216
-
217
- def c_multiple_attribute(attr_xml, node)
218
- rm_attribute_name = attr_xml.at('rm_attribute_name').text
219
- existence = occurrences(attr_xml.at('existence'))
220
- 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))
221
- end
222
-
223
- def c_code_phrase(attr_xml, node)
224
- terminology_id_node = attr_xml.at('terminology_id/value')
225
- terminology_id = terminology_id_node ? OpenEHR::RM::Support::Identification::TerminologyID.new(value: terminology_id_node.text.strip) : nil
226
-
227
- code_list_nodes = attr_xml.xpath('code_list')
228
- code_list = code_list_nodes.map { |code_node| code_node.text.strip }
229
- code_list = [code_list.first] if code_list.size == 1 && code_list.first.empty?
230
-
231
- occurrences_node = attr_xml.at('occurrences')
232
- occurrences_obj = occurrences_node ? occurrences(occurrences_node) : nil
233
-
234
- OpenEHR::AM::OpenEHRProfile::DataTypes::Text::CCodePhrase.new(
235
- terminology_id: terminology_id,
236
- code_list: code_list,
237
- path: node.path,
238
- occurrences: occurrences_obj,
239
- rm_type_name: 'CODE_PHRASE'
240
- )
241
- end
242
-
243
- def archetype_slot(attr_xml,node)
244
- path = node.path
245
- node.id = attr_xml.at('node_id').text
246
- rm_type_name = attr_xml.at('rm_type_name').text
247
- occurrences = occurrences(attr_xml.at('occurrences'))
248
- includes_leaf = attr_xml.at('includes')
249
- includes = assertions(includes_leaf.children, node) if includes_leaf
250
- excludes_leaf = attr_xml.at('excludes')
251
- excludes = assertions(excludes_leaf.children, node) if excludes_leaf
252
- OpenEHR::AM::Archetype::ConstraintModel::ArchetypeSlot.new(path: path, node_id: node.id, rm_type_name: rm_type_name, occurrences: occurrences, includes: includes, excludes: excludes)
253
- end
254
-
255
- def occurrences(occurrence_xml)
256
- return nil if occurrence_xml.nil?
257
-
258
- lower_node = occurrence_xml.at('lower')
259
- upper_node = occurrence_xml.at('upper')
260
- lower_included_node = occurrence_xml.at('lower_included')
261
- upper_included_node = occurrence_xml.at('upper_included')
262
- lower_unbounded_node = occurrence_xml.at('lower_unbounded')
263
- upper_unbounded_node = occurrence_xml.at('upper_unbounded')
264
-
265
- lower = lower_node ? lower_node.text.to_i : nil
266
- upper = upper_node ? upper_node.text.to_i : nil
267
- lower_included = lower_included_node ? to_bool(lower_included_node.text) : (lower.nil? ? nil : true)
268
- upper_included = upper_included_node ? to_bool(upper_included_node.text) : (upper.nil? ? nil : true)
269
- lower_unbounded = lower_unbounded_node ? to_bool(lower_unbounded_node.text) : false
270
- upper_unbounded = upper_unbounded_node ? to_bool(upper_unbounded_node.text) : false
271
-
272
- # An occurrences element with none of its children present carries no
273
- # constraint at all; Interval requires at least one bound, so treat
274
- # this as "no occurrences data" rather than raising.
275
- return nil if lower.nil? && upper.nil? && !lower_unbounded && !upper_unbounded
276
-
277
- # Handle unbounded intervals properly
278
- if upper_unbounded || upper.nil?
279
- upper = nil
280
- upper_included = nil
281
- end
282
-
283
- if lower_unbounded || lower.nil?
284
- lower = nil
285
- lower_included = nil
286
- end
287
-
288
- OpenEHR::AssumedLibraryTypes::Interval.new(
289
- lower: lower,
290
- upper: upper,
291
- lower_included: lower_included,
292
- upper_included: upper_included
293
- )
294
- end
295
-
296
- def cardinality(xml)
297
- return nil if xml.nil?
298
-
299
- order_node = xml.at('is_ordered')
300
- unique_node = xml.at('is_unique')
301
- interval_node = xml.at('interval')
302
-
303
- # No cardinality sub-elements at all means no cardinality data.
304
- return nil if order_node.nil? && unique_node.nil? && interval_node.nil?
305
-
306
- order = order_node ? to_bool(order_node.text) : false
307
- unique = unique_node ? to_bool(unique_node.text) : false
308
- interval = interval_node ? occurrences(interval_node) : nil
309
-
310
- OpenEHR::AM::Archetype::ConstraintModel::Cardinality.new(
311
- is_ordered: order,
312
- is_unique: unique,
313
- interval: interval
314
- )
315
- end
316
-
317
- def archetype_internal_ref(attr_xml, node)
318
- rm_type_name = attr_xml.at('rm_type_name').text
319
- target_path = attr_xml.at('target_path').text
320
- occurrences = occurrences(attr_xml.at('occurrences'))
321
- OpenEHR::AM::Archetype::ConstraintModel::ArchetypeInternalRef.new(rm_type_name: rm_type_name, occurrences: occurrences, target_path: target_path)
322
- end
323
-
324
- # No .opt fixture in this gem's corpus uses a C_DV_STATE (state
325
- # machine) constraint, so its actual OPT XML shape is unverified;
326
- # raising a clear, documented error here is safer than guessing
327
- # at element names and risking a silently wrong StateMachine.
328
- def c_dv_state(_attr_xml, _node)
329
- raise NotImplementedError, 'OPTParser does not yet support C_DV_STATE (state machine) constraints'
330
- end
331
-
332
- def constraint_ref(attr_xml, node)
333
- rm_type_name = attr_xml.at('rm_type_name').text
334
- reference = attr_xml.at('reference').text
335
- occurrences = occurrences(attr_xml.at('occurrences'))
336
- OpenEHR::AM::Archetype::ConstraintModel::ConstraintRef.new(rm_type_name: rm_type_name, occurrences: occurrences, reference: reference)
337
- end
338
-
339
- def assertions(attr_xml, node)
340
- string_expression = attr_xml.at('string_expression')
341
- string_expression = string_expression.nil? ? nil : string_expression.text
342
- expression_leaf = attr_xml.at 'expression'
343
- expression = send expression_leaf.attributes['type'].text.downcase, expression_leaf
344
- [OpenEHR::AM::Archetype::Assertion::Assertion.new(expression: expression, string_expression: string_expression)]
345
- end
346
-
347
- def expr_binary_operator(attr_xml)
348
- type = attr_xml.at('type').text
349
- operator = OpenEHR::AM::Archetype::Assertion::OperatorKind.new(value: attr_xml.at('operator').text.to_i)
350
-
351
- precedence_overridden = attr_xml.at('precedence_overridden').text == 'true' ? true : false
352
- right_operand_leaf = attr_xml.at 'right_operand'
353
- right_operand = send right_operand_leaf.attributes['type'].text.downcase, right_operand_leaf
354
- left_operand_leaf = attr_xml.at 'left_operand'
355
- left_operand = send left_operand_leaf.attributes['type'].text.downcase, left_operand_leaf
356
- OpenEHR::AM::Archetype::Assertion::ExprBinaryOperator.new(type: type, operator: operator, precedence_overridden: precedence_overridden, right_operand: right_operand, left_operand: left_operand)
357
- end
358
-
359
- def expr_leaf(attr_xml)
360
- type = attr_xml.at('type').text
361
- item_leaf = attr_xml.at('item')
362
- item = send type.downcase, item_leaf
363
- reference_type = attr_xml.at('reference_type').text
364
- OpenEHR::AM::Archetype::Assertion::ExprLeaf.new(type: type, item: item, reference_type: reference_type)
365
- end
366
-
367
- def c_primitive_object(attr_xml, node)
368
- rm_type_name = attr_xml.at('rm_type_name').text
369
- occurrences = occurrences(attr_xml.at('occurrences'))
370
- item = send attr_xml.at('item')['type'].downcase, attr_xml.at('item')
371
- OpenEHR::AM::Archetype::ConstraintModel::CPrimitiveObject.new(rm_type_name: rm_type_name, occurrences: occurrences, node_id: node.id, item: item)
372
- end
373
-
374
- def c_string(attr_xml)
375
- if attr_xml.at('pattern')
376
- OpenEHR::AM::Archetype::ConstraintModel::Primitive::CString.new(pattern: attr_xml.at('pattern').text)
377
- else
378
- list = attr_xml.xpath('.//list').map do |str|
379
- str.text
380
- end
381
- OpenEHR::AM::Archetype::ConstraintModel::Primitive::CString.new(list: list)
382
- end
383
- end
384
-
385
- def c_dv_quantity(attr_xml, node)
386
- rm_type_name = attr_xml.at('rm_type_name').text
387
- occurrences = occurrences(attr_xml.at('occurrences'))
388
- property = property_code_phrase(attr_xml.at('property'))
389
- list = attr_xml.xpath('.//list').map do |element|
390
- units = element.at('units').text if element.at('units')
391
- magnitude = occurrences(element.at('magnitude')) if element.at('magnitude')
392
- precision = occurrences(element.at('precision')) if element.at('precision')
393
- OpenEHR::AM::OpenEHRProfile::DataTypes::Quantity::CQuantityItem.new(magnitude: magnitude, precision: precision, units: units)
394
- end
395
- OpenEHR::AM::OpenEHRProfile::DataTypes::Quantity::CDvQuantity.new(rm_type_name: rm_type_name, occurrences: occurrences, list: list, property: property)
396
- end
397
-
398
- # The <property> element is optional in real templates; return nil rather
399
- # than dereferencing missing terminology/code nodes.
400
- def property_code_phrase(property_xml)
401
- return nil if property_xml.nil?
402
- terminology_node = property_xml.at('terminology_id/value')
403
- code_node = property_xml.at('code_string')
404
- return nil if terminology_node.nil? || code_node.nil?
405
- terminology_id = OpenEHR::RM::Support::Identification::TerminologyID.new(value: terminology_node.text)
406
- OpenEHR::RM::DataTypes::Text::CodePhrase.new(terminology_id: terminology_id, code_string: code_node.text)
407
- end
408
-
409
- def c_dv_ordinal(attr_xml, node)
410
- rm_type_name = attr_xml.at('rm_type_name').text
411
- occurrences = occurrences(attr_xml.at('occurrences'))
412
- list = attr_xml.xpath('list').map { |element| dv_ordinal_item(element) }.compact
413
- OpenEHR::AM::OpenEHRProfile::DataTypes::Quantity::CDvOrdinal.new(rm_type_name: rm_type_name, occurrences: occurrences, list: list)
414
- end
415
-
416
- # DV_ORDINAL.symbol is spec'd as DV_CODED_TEXT; the OPT XML only
417
- # carries a defining_code (terminology_id + code_string), so the
418
- # DvCodedText's own value is set to that same code_string (there
419
- # is no separate display text in this element).
420
- def dv_ordinal_item(element)
421
- value_node = element.at('value')
422
- return nil unless value_node && !value_node.text.empty?
423
-
424
- code_phrase = property_code_phrase(element.at('symbol/defining_code'))
425
- return nil if code_phrase.nil?
426
-
427
- symbol = OpenEHR::RM::DataTypes::Text::DvCodedText.new(value: code_phrase.code_string, defining_code: code_phrase)
428
- OpenEHR::RM::DataTypes::Quantity::DvOrdinal.new(value: value_node.text.to_i, symbol: symbol)
429
- end
430
-
431
- def c_dv_scale(attr_xml, node)
432
- rm_type_name = attr_xml.at('rm_type_name').text
433
- occurrences = occurrences(attr_xml.at('occurrences'))
434
- list = attr_xml.xpath('list').map { |element| dv_scale_item(element) }.compact
435
- OpenEHR::AM::OpenEHRProfile::DataTypes::Quantity::CDvScale.new(rm_type_name: rm_type_name, occurrences: occurrences, list: list)
436
- end
437
-
438
- # Same XML shape as C_DV_ORDINAL's list items, but DV_SCALE.value
439
- # is Real rather than Integer.
440
- def dv_scale_item(element)
441
- value_node = element.at('value')
442
- return nil unless value_node && !value_node.text.empty?
443
-
444
- code_phrase = property_code_phrase(element.at('symbol/defining_code'))
445
- return nil if code_phrase.nil?
446
-
447
- symbol = OpenEHR::RM::DataTypes::Text::DvCodedText.new(value: code_phrase.code_string, defining_code: code_phrase)
448
- OpenEHR::RM::DataTypes::Quantity::DvScale.new(value: value_node.text.to_f, symbol: symbol)
449
- end
450
-
451
- def c_date(xml)
452
- pattern = xml.at('pattern')
453
- range = xml.at('range')
454
- if pattern
455
- OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDate.new(pattern: pattern.text)
456
- elsif range
457
- OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDate.new(range: occurrences(range))
458
- else
459
- OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDate.new
460
- end
461
- end
462
-
463
- def c_date_time(xml)
464
- pattern = xml.at('pattern')
465
- range = xml.at('range')
466
- if pattern
467
- OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDateTime.new(pattern: pattern.text)
468
- elsif range
469
- OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDateTime.new(range: occurrences(range))
470
- else
471
- OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDateTime.new
472
- end
473
- end
474
-
475
- def c_integer(xml)
476
- range = xml.at('range')
477
- list = xml.xpath('list')
478
- if range
479
- OpenEHR::AM::Archetype::ConstraintModel::Primitive::CInteger.new(range: occurrences(range))
480
- elsif !list.empty?
481
- list_values = list.map { |item| item.text.to_i }
482
- OpenEHR::AM::Archetype::ConstraintModel::Primitive::CInteger.new(list: list_values)
483
- else
484
- OpenEHR::AM::Archetype::ConstraintModel::Primitive::CInteger.new
485
- end
486
- end
487
-
488
- def c_real(xml)
489
- range = xml.at('range')
490
- list = xml.xpath('list')
491
- if range
492
- OpenEHR::AM::Archetype::ConstraintModel::Primitive::CReal.new(range: occurrences(range))
493
- elsif !list.empty?
494
- list_values = list.map { |item| item.text.to_f }
495
- OpenEHR::AM::Archetype::ConstraintModel::Primitive::CReal.new(list: list_values)
496
- else
497
- OpenEHR::AM::Archetype::ConstraintModel::Primitive::CReal.new
498
- end
499
- end
500
-
501
- def c_duration(xml)
502
- pattern = xml.at('pattern')
503
- range_xml = xml.at('range')
504
- list = xml.xpath('list')
505
- if pattern
506
- OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDuration.new(pattern: pattern.text)
507
- elsif range_xml
508
- OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDuration.new(range: duration_range(range_xml))
509
- elsif !list.empty?
510
- OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDuration.new(list: list.map(&:text))
511
- else
512
- OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDuration.new
513
- end
514
- end
515
-
516
- # A C_DURATION range's bounds are ISO8601 duration strings (e.g.
517
- # PT24H), not plain numbers, so occurrences() (built for numeric
518
- # Interval bounds) doesn't apply here; wrap each bound as a
519
- # DV_DURATION instead, matching what CDuration#valid_value?
520
- # already expects its range bounds to be.
521
- def duration_range(range_xml)
522
- lower = duration_bound(range_xml.at('lower'), range_xml.at('lower_unbounded'))
523
- upper = duration_bound(range_xml.at('upper'), range_xml.at('upper_unbounded'))
524
- return nil if lower.nil? && upper.nil?
525
-
526
- OpenEHR::AssumedLibraryTypes::Interval.new(
527
- lower: lower, upper: upper,
528
- lower_included: lower.nil? ? nil : bool_node(range_xml.at('lower_included'), true),
529
- upper_included: upper.nil? ? nil : bool_node(range_xml.at('upper_included'), true))
530
- end
531
-
532
- def duration_bound(value_node, unbounded_node)
533
- return nil if bool_node(unbounded_node, false)
534
- return nil if value_node.nil? || value_node.text.empty?
535
-
536
- OpenEHR::RM::DataTypes::Quantity::DateTime::DvDuration.new(value: value_node.text)
537
- end
538
-
539
- def bool_node(node, default)
540
- node ? to_bool(node.text) : default
541
- end
542
-
543
- def c_time(xml)
544
- pattern = xml.at('pattern')
545
- range = xml.at('range')
546
- if pattern
547
- OpenEHR::AM::Archetype::ConstraintModel::Primitive::CTime.new(pattern: pattern.text)
548
- elsif range
549
- OpenEHR::AM::Archetype::ConstraintModel::Primitive::CTime.new(range: occurrences(range))
550
- else
551
- OpenEHR::AM::Archetype::ConstraintModel::Primitive::CTime.new
552
- end
553
- end
554
-
555
- def c_boolean(xml)
556
- true_valid = xml.at('true_valid')
557
- false_valid = xml.at('false_valid')
558
- assumed_value = xml.at('assumed_value')
559
-
560
- true_valid_value = true_valid ? to_bool(true_valid.text) : nil
561
- false_valid_value = false_valid ? to_bool(false_valid.text) : nil
562
- assumed_value_value = assumed_value ? to_bool(assumed_value.text) : nil
563
-
564
- OpenEHR::AM::Archetype::ConstraintModel::Primitive::CBoolean.new(
565
- true_valid: true_valid_value,
566
- false_valid: false_valid_value,
567
- assumed_value: assumed_value_value
568
- )
569
- end
570
-
571
- def string(attr_xml)
572
- attr_xml.text
573
- end
574
-
575
170
  def empty_then_nil(val)
576
171
  if val.empty?
577
172
  return nil
@@ -583,14 +178,6 @@ module OpenEHR
583
178
  def text_on_path(xml, path)
584
179
  xml.xpath(path).text
585
180
  end
586
-
587
- def to_bool(str)
588
- return nil if str.nil?
589
- str = str.text if str.respond_to?(:text)
590
- return true if /true/i =~ str.to_s
591
- return false if /false/i =~ str.to_s
592
- nil
593
- end
594
181
  end
595
182
  end
596
183
  end