openehr 1.3.0 → 2.0.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.rdoc +93 -12
  3. data/lib/openehr/am/archetype/constraint_model/primitive.rb +204 -6
  4. data/lib/openehr/am/archetype/constraint_model.rb +228 -4
  5. data/lib/openehr/am/archetype/ontology.rb +2 -2
  6. data/lib/openehr/am/archetype.rb +101 -2
  7. data/lib/openehr/am/openehr_profile/data_types/basic.rb +28 -0
  8. data/lib/openehr/am/openehr_profile/data_types/quantity.rb +88 -0
  9. data/lib/openehr/am/openehr_profile/data_types/text.rb +23 -0
  10. data/lib/openehr/am/template.rb +25 -3
  11. data/lib/openehr/aql/engine/binding.rb +13 -0
  12. data/lib/openehr/aql/engine/contains_resolver.rb +161 -0
  13. data/lib/openehr/aql/engine/dataset.rb +122 -0
  14. data/lib/openehr/aql/engine/path_evaluator.rb +137 -0
  15. data/lib/openehr/aql/engine/predicate_evaluator.rb +65 -0
  16. data/lib/openehr/aql/engine.rb +119 -0
  17. data/lib/openehr/aql/errors.rb +27 -0
  18. data/lib/openehr/aql/lexer.rb +295 -0
  19. data/lib/openehr/aql/model/containment.rb +45 -0
  20. data/lib/openehr/aql/model/from_clause.rb +35 -0
  21. data/lib/openehr/aql/model/function_call.rb +56 -0
  22. data/lib/openehr/aql/model/identified_path.rb +20 -0
  23. data/lib/openehr/aql/model/literal.rb +15 -0
  24. data/lib/openehr/aql/model/object_path.rb +34 -0
  25. data/lib/openehr/aql/model/order_by_and_limit.rb +50 -0
  26. data/lib/openehr/aql/model/predicate.rb +84 -0
  27. data/lib/openehr/aql/model/query.rb +24 -0
  28. data/lib/openehr/aql/model/select_clause.rb +29 -0
  29. data/lib/openehr/aql/model/where_clause.rb +105 -0
  30. data/lib/openehr/aql/model.rb +14 -0
  31. data/lib/openehr/aql/parser.rb +484 -0
  32. data/lib/openehr/aql/result_set.rb +54 -0
  33. data/lib/openehr/aql.rb +28 -0
  34. data/lib/openehr/assumed_library_types.rb +68 -17
  35. data/lib/openehr/parser/adl_grammar.tt +29 -13
  36. data/lib/openehr/parser/adl_parser.rb +18 -4
  37. data/lib/openehr/parser/archetype_validator.rb +126 -0
  38. data/lib/openehr/parser/exception.rb +13 -0
  39. data/lib/openehr/parser/opt_parser.rb +162 -9
  40. data/lib/openehr/parser.rb +2 -0
  41. data/lib/openehr/path.rb +195 -0
  42. data/lib/openehr/rm/common/archetyped.rb +116 -6
  43. data/lib/openehr/rm/common/change_control.rb +3 -8
  44. data/lib/openehr/rm/common/directory.rb +4 -0
  45. data/lib/openehr/rm/common/generic.rb +24 -10
  46. data/lib/openehr/rm/composition/content/entry.rb +24 -5
  47. data/lib/openehr/rm/composition/content/navigation.rb +2 -1
  48. data/lib/openehr/rm/composition.rb +19 -2
  49. data/lib/openehr/rm/data_structures/history.rb +3 -0
  50. data/lib/openehr/rm/data_structures/item_structure/representation.rb +13 -9
  51. data/lib/openehr/rm/data_structures/item_structure.rb +28 -15
  52. data/lib/openehr/rm/data_types/encapsulated.rb +23 -3
  53. data/lib/openehr/rm/data_types/quantity/date_time.rb +9 -1
  54. data/lib/openehr/rm/data_types/quantity.rb +66 -14
  55. data/lib/openehr/rm/data_types/text.rb +8 -0
  56. data/lib/openehr/rm/data_types/time_specification.rb +5 -4
  57. data/lib/openehr/rm/demographic.rb +4 -3
  58. data/lib/openehr/rm/ehr.rb +20 -1
  59. data/lib/openehr/rm/factory.rb +234 -6
  60. data/lib/openehr/rm/integration.rb +1 -0
  61. data/lib/openehr/rm/support/identification.rb +37 -8
  62. data/lib/openehr/rm/support/measurement.rb +32 -0
  63. data/lib/openehr/rm/type_name.rb +90 -0
  64. data/lib/openehr/rm.rb +3 -0
  65. data/lib/openehr/serializer/adl_serializer.rb +335 -0
  66. data/lib/openehr/serializer/base.rb +20 -0
  67. data/lib/openehr/serializer/opt_serializer.rb +49 -0
  68. data/lib/openehr/serializer/rm_json_serializer.rb +62 -0
  69. data/lib/openehr/serializer/xml_serializer.rb +260 -0
  70. data/lib/openehr/serializer.rb +5 -291
  71. data/lib/openehr/terminology_service.rb +44 -0
  72. data/lib/openehr/version.rb +1 -1
  73. data/lib/openehr.rb +5 -2
  74. metadata +35 -7
  75. data/lib/openehr/parser/validator.rb +0 -18
  76. data/lib/openehr/parser/xml_parser.rb +0 -13
  77. data/lib/openehr/rm/data_types/charset_extract.rb +0 -24
  78. data/lib/openehr/writer.rb +0 -12
@@ -37,6 +37,19 @@ module OpenEHR
37
37
  def parent_path
38
38
  parent ? parent.path : ''
39
39
  end
40
+
41
+ # Nil-safe interval subset check shared by the
42
+ # occurrences/existence/cardinality conformance rules: an
43
+ # unconstrained parent (nil) accepts anything, an
44
+ # unconstrained child (nil) only conforms to an
45
+ # unconstrained parent, and otherwise the child interval
46
+ # must be a genuine subset of the parent's.
47
+ def interval_conforms_to?(mine, other)
48
+ return true if other.nil?
49
+ return false if mine.nil?
50
+
51
+ mine.subset_of?(other)
52
+ end
40
53
  end
41
54
 
42
55
  class Cardinality
@@ -118,8 +131,30 @@ module OpenEHR
118
131
  @rm ||= OpenEHR::RM::Factory.create(rm_type_name, params)
119
132
  end
120
133
 
134
+ # Whether this (potentially specialised) node is a legal
135
+ # redefinition of +other+, the corresponding node in the
136
+ # flat parent archetype: same node_id lineage (at0001.1
137
+ # specialises at0001), same rm_type_name (subtype checking
138
+ # is deferred until the RM type-name helper lands), and
139
+ # occurrences no wider than the parent's.
140
+ def node_conforms_to?(other)
141
+ return false if other.nil?
142
+
143
+ node_id_conforms_to?(other.node_id) &&
144
+ rm_type_name == other.rm_type_name &&
145
+ interval_conforms_to?(occurrences, other.occurrences)
146
+ end
147
+
121
148
  private
122
149
 
150
+ def node_id_conforms_to?(other_node_id)
151
+ return true if node_id == other_node_id
152
+ return false if node_id.nil? || other_node_id.nil?
153
+
154
+ node_id.start_with?(other_node_id + '.') &&
155
+ node_id[(other_node_id.length + 1)..-1] =~ /\A\d+(\.\d+)*\z/
156
+ end
157
+
123
158
  def calculate_path
124
159
  path_left_part = parent_path
125
160
  path_left_part = '/' if path_left_part == ''
@@ -177,6 +212,16 @@ module OpenEHR
177
212
  @path || calculate_path
178
213
  end
179
214
 
215
+ # Whether this (potentially specialised) attribute node is
216
+ # a legal redefinition of +other+: same rm_attribute_name
217
+ # and an existence no wider than the parent's.
218
+ def node_conforms_to?(other)
219
+ return false if other.nil?
220
+
221
+ rm_attribute_name == other.rm_attribute_name &&
222
+ interval_conforms_to?(existence, other.existence)
223
+ end
224
+
180
225
  private
181
226
 
182
227
  def calculate_path
@@ -224,9 +269,9 @@ module OpenEHR
224
269
  return item.nil?
225
270
  end
226
271
 
227
- %w(assumed_value assumed_value= has_assumed_value? default_value
228
- any_allowed?, valid_value?).each do |m|
229
- define_method(m) do |*args|
272
+ %w(assumed_value assumed_value= has_assumed_value? default_value
273
+ valid_value?).each do |m|
274
+ define_method(m) do |*args|
230
275
  self.item.send(m, *args) if !self.item.nil?
231
276
  end
232
277
  end
@@ -263,6 +308,175 @@ module OpenEHR
263
308
  def any_allowed?
264
309
  return (@attributes.nil? or @attributes.empty?)
265
310
  end
311
+
312
+ # Recursively checks +value+ (an RM instance) against this
313
+ # node's constraints: rm_type_name conformance (RM subtypes
314
+ # allowed), archetype_node_id (when this node specifies
315
+ # one), then each attribute's existence/cardinality and its
316
+ # children constraints. ArchetypeSlot/ArchetypeInternalRef/
317
+ # ConstraintRef children are accepted permissively (v1 does
318
+ # not resolve slot-fillers or internal references).
319
+ def valid_value?(value)
320
+ return false if value.nil?
321
+ return false unless node_id_matches?(value)
322
+ return false unless OpenEHR::RM.subtype_of?(value, rm_type_name)
323
+ return true if any_allowed?
324
+
325
+ attributes.all? { |attribute| attribute_conforms?(attribute, attribute_values(attribute, value)) }
326
+ end
327
+
328
+ # Diagnostic counterpart to valid_value?, for building
329
+ # path-annotated instance-validation reports: walks the same
330
+ # rules but returns the first [failing_constraint_node,
331
+ # failing_rm_value] pair found (depth-first) instead of a
332
+ # bare boolean, or nil when +value+ fully conforms.
333
+ def find_violation(value)
334
+ if value.nil? || !node_id_matches?(value) || !OpenEHR::RM.subtype_of?(value, rm_type_name)
335
+ return [self, value]
336
+ end
337
+ return nil if any_allowed?
338
+
339
+ attributes.each do |attribute|
340
+ violation = attribute_violation(attribute, value)
341
+ return violation if violation
342
+ end
343
+ nil
344
+ end
345
+
346
+ private
347
+
348
+ def attribute_violation(attribute, value)
349
+ rm_values = attribute_values(attribute, value)
350
+ return [self, value] unless interval_satisfied?(attribute.existence, rm_values.empty? ? 0 : 1)
351
+
352
+ if attribute.is_a?(CMultipleAttribute)
353
+ return [self, value] unless interval_satisfied?(attribute.cardinality&.interval, rm_values.size)
354
+ return [self, value] unless children_occurrences_satisfied?(attribute.children, rm_values)
355
+ end
356
+
357
+ children = attribute.children || []
358
+ rm_values.each do |v|
359
+ matched = children.find { |child| child_matches?(child, v) }
360
+ return [self, v] if matched.nil?
361
+ next unless matched.respond_to?(:find_violation)
362
+
363
+ violation = matched.find_violation(v)
364
+ return violation if violation
365
+ end
366
+ nil
367
+ end
368
+
369
+ def node_id_matches?(value)
370
+ return true if node_id.nil?
371
+ return true unless value.respond_to?(:archetype_node_id)
372
+
373
+ value.archetype_node_id == node_id
374
+ end
375
+
376
+ def attribute_values(attribute, value)
377
+ name = attribute.rm_attribute_name
378
+ Array(value.respond_to?(name) ? value.send(name) : nil)
379
+ end
380
+
381
+ def attribute_conforms?(attribute, rm_values)
382
+ present = rm_values.empty? ? 0 : 1
383
+ return false unless interval_satisfied?(attribute.existence, present)
384
+
385
+ if attribute.is_a?(CMultipleAttribute)
386
+ return false unless interval_satisfied?(attribute.cardinality&.interval, rm_values.size)
387
+ return false unless children_occurrences_satisfied?(attribute.children, rm_values)
388
+ end
389
+
390
+ children = attribute.children || []
391
+ rm_values.all? { |v| children.any? { |child| child_matches?(child, v) } }
392
+ end
393
+
394
+ def interval_satisfied?(interval, count)
395
+ interval.nil? || interval.has?(count)
396
+ end
397
+
398
+ def children_occurrences_satisfied?(children, rm_values)
399
+ (children || []).all? do |child|
400
+ next true if child.occurrences.nil?
401
+
402
+ child.occurrences.has?(rm_values.count { |v| child_matches?(child, v) })
403
+ end
404
+ end
405
+
406
+ def child_matches?(child, rm_value)
407
+ return true if child.is_a?(ArchetypeSlot) ||
408
+ child.is_a?(ArchetypeInternalRef) ||
409
+ child.is_a?(ConstraintRef)
410
+
411
+ child.valid_value?(rm_value)
412
+ end
413
+
414
+ public
415
+
416
+ # Builds the minimal RM instance satisfying this node's
417
+ # mandatory constraints: only attributes with existence
418
+ # lower >= 1 are populated, each from its first child
419
+ # alternative's own default_value (recursively), and
420
+ # CMultipleAttribute gets exactly cardinality.interval.lower
421
+ # (or 1) copies of it. Returns nil - rather than raising -
422
+ # whenever a mandatory field has no derivable default, since
423
+ # that means no minimal instance can be built at all.
424
+ def default_value
425
+ return nil if any_allowed?
426
+
427
+ klass = OpenEHR::RM.class_for(rm_type_name)
428
+ return nil if klass.nil?
429
+
430
+ params = mandatory_attribute_params
431
+ return nil if params.nil?
432
+
433
+ klass.new(node_id.nil? ? params : params.merge(:archetype_node_id => node_id))
434
+ rescue ArgumentError, NotImplementedError
435
+ nil
436
+ end
437
+
438
+ private
439
+
440
+ def mandatory_attribute_params
441
+ attributes.each_with_object({}) do |attribute, params|
442
+ next unless mandatory_attribute?(attribute)
443
+
444
+ value = attribute_default(attribute)
445
+ return nil if value.nil?
446
+
447
+ params[attribute.rm_attribute_name.to_sym] = value
448
+ end
449
+ end
450
+
451
+ def mandatory_attribute?(attribute)
452
+ attribute.existence.nil? || attribute.existence.lower.to_i >= 1
453
+ end
454
+
455
+ def attribute_default(attribute)
456
+ children = attribute.children || []
457
+ return nil if children.empty?
458
+
459
+ child = children.first
460
+ return nil if child.is_a?(ArchetypeSlot) ||
461
+ child.is_a?(ArchetypeInternalRef) ||
462
+ child.is_a?(ConstraintRef)
463
+
464
+ if attribute.is_a?(CMultipleAttribute)
465
+ multiple_default(child, attribute.cardinality)
466
+ else
467
+ child.default_value
468
+ end
469
+ end
470
+
471
+ def multiple_default(child, cardinality)
472
+ value = child.default_value
473
+ return nil if value.nil?
474
+
475
+ count = cardinality&.interval&.lower
476
+ count = 1 if count.nil? || count < 1
477
+
478
+ Array.new(count) { value }
479
+ end
266
480
  end
267
481
 
268
482
  class CDomainType < CDefinedObject
@@ -370,12 +584,22 @@ module OpenEHR
370
584
 
371
585
  class CMultipleAttribute < CAttribute
372
586
  attr_accessor :members, :cardinality
373
-
587
+
374
588
  def initialize(args = { })
375
589
  super
376
590
  self.members = args[:members]
377
591
  self.cardinality = args[:cardinality]
378
592
  end
593
+
594
+ # Adds cardinality⊆ on top of CAttribute's rm_attribute_name
595
+ # and existence conformance rules.
596
+ def node_conforms_to?(other)
597
+ return false unless super
598
+
599
+ mine = cardinality ? cardinality.interval : nil
600
+ others = other.cardinality ? other.cardinality.interval : nil
601
+ interval_conforms_to?(mine, others)
602
+ end
379
603
  end
380
604
  end # of ConstraintModel
381
605
  end # of Archetype
@@ -77,8 +77,8 @@ module OpenEHR
77
77
 
78
78
  protected
79
79
  def code_set(definitions)
80
- codes = definitions.values.inject([]) do |codes, item|
81
- item.keys
80
+ codes = definitions.values.inject([]) do |accumulated, item|
81
+ accumulated + item.keys.to_a
82
82
  end
83
83
  codes.uniq
84
84
  end
@@ -65,33 +65,132 @@ module OpenEHR
65
65
  return @ontology.term_definition(:lang => a_lang, :code => @concept).items['text']
66
66
  end
67
67
 
68
+ NODE_ID_PATTERN = /\A(?:at|ac|id)\d+(\.\d+)*\z/
69
+
70
+ # A reference to a constraint definition (ADL "matches {use_node ac0001}")
71
+ # is valid when it names a code actually present in the ontology's
72
+ # constraint_definitions. True when there are no such references at
73
+ # all; false (rather than vacuously true) when there are references
74
+ # but the ontology has no constraint_definitions section.
68
75
  def constraint_references_valid?
76
+ refs = collect(ConstraintModel::ConstraintRef) { |node| node.reference }
77
+ return true if refs.empty?
78
+
79
+ codes = ontology.constraint_codes
80
+ return false if codes.nil?
81
+
82
+ refs.all? { |ref| codes.include?(ref) }
69
83
  end
70
84
 
85
+ # An ARCHETYPE_INTERNAL_REF's target_path must resolve to a node
86
+ # physically present in this same definition tree.
71
87
  def internal_references_valid?
88
+ refs = collect(ConstraintModel::ArchetypeInternalRef) { |node| node.target_path }
89
+ paths = physical_paths
90
+ refs.all? { |ref| paths.include?(ref) }
72
91
  end
73
92
 
93
+ # True when a specialise header was present (i.e. this archetype
94
+ # names a parent archetype to specialise).
74
95
  def is_specialised?
96
+ !parent_archetype_id.nil?
75
97
  end
76
98
 
99
+ # Composition of the three granular structural checks above.
77
100
  def is_valid?
101
+ node_ids_valid? && constraint_references_valid? && internal_references_valid?
78
102
  end
79
103
 
80
- def logical_paths(a_lang)
104
+ # physical_paths with each at/ac/id-code predicate replaced by its
105
+ # ontology term text for +a_lang+ (falling back to the bare code
106
+ # when no term text is found for it).
107
+ def logical_paths(a_lang)
108
+ physical_paths.map { |path| localize_path(path, a_lang) }
109
+ end
110
+
111
+ # Every node_id used in the definition, syntactically valid
112
+ # (at/ac/id-code, optionally dotted for specialisation) and present
113
+ # in the ontology's term codes.
114
+ def node_ids_valid?
115
+ node_ids = collect_node_ids
116
+ codes = ontology.term_codes
117
+ node_ids.all? { |id| id =~ NODE_ID_PATTERN && codes.include?(id) }
81
118
  end
82
119
 
83
120
  def node_ids_vaild?
121
+ warn '[DEPRECATED] node_ids_vaild? is deprecated (typo); use node_ids_valid? instead'
122
+ node_ids_valid?
84
123
  end
85
124
 
125
+ # The archetype path of every node physically present in the
126
+ # definition, in depth-first order, without duplicates (a node
127
+ # without its own node_id shares its parent attribute's path).
86
128
  def physical_paths
129
+ paths = []
130
+ each_constraint_node(definition) { |node| paths << node.path if node.respond_to?(:path) }
131
+ paths.uniq
87
132
  end
88
133
 
134
+ # This gem does not parse ADL revision history, so a previous
135
+ # version can never be determined from a parsed archetype.
89
136
  def previous_version
137
+ nil
90
138
  end
91
139
 
140
+ # Prefers the ontology's own (parser-derived) specialisation_depth
141
+ # when available; otherwise falls back to what the archetype_id's
142
+ # domain_concept specialisation suffix can tell us. ArchetypeID
143
+ # currently only represents a single specialisation level, so this
144
+ # fallback can only ever return 0 or 1.
92
145
  def specialisation_depth
146
+ return ontology.specialisation_depth if ontology.specialisation_depth
147
+
148
+ archetype_id.specialisation.nil? ? 0 : 1
149
+ end
150
+
151
+ private
152
+
153
+ def each_constraint_node(node, &block)
154
+ return if node.nil?
155
+
156
+ yield node
157
+ if node.respond_to?(:attributes) && node.attributes
158
+ node.attributes.each { |attribute| each_constraint_node(attribute, &block) }
159
+ end
160
+ return unless node.respond_to?(:children) && node.children
161
+
162
+ node.children.each { |child| each_constraint_node(child, &block) }
93
163
  end
94
-
164
+
165
+ def collect(klass)
166
+ found = []
167
+ each_constraint_node(definition) { |node| found << yield(node) if node.is_a?(klass) }
168
+ found
169
+ end
170
+
171
+ def collect_node_ids
172
+ ids = []
173
+ each_constraint_node(definition) { |node| ids << node.node_id if node.respond_to?(:node_id) && node.node_id }
174
+ ids
175
+ end
176
+
177
+ def localize_path(path, a_lang)
178
+ path.gsub(/\[((?:at|ac|id)\d+(?:\.\d+)*)\]/) do
179
+ code = Regexp.last_match(1)
180
+ text = term_text(code, a_lang)
181
+ text ? "[#{text}]" : "[#{code}]"
182
+ end
183
+ end
184
+
185
+ def term_text(code, a_lang)
186
+ term = ontology.term_definition(:lang => a_lang, :code => code)
187
+ term && term.items['text']
188
+ rescue StandardError
189
+ nil
190
+ end
191
+
192
+ public
193
+
95
194
  def self.create(args ={}, &block)
96
195
  archetype = new(args)
97
196
  if block_given?
@@ -18,6 +18,34 @@ module OpenEHR
18
18
  raise ArgumentError, 'value is mandatory' if value.nil?
19
19
  @value = value
20
20
  end
21
+
22
+ def valid_value?(rm_value)
23
+ return false if rm_value.nil?
24
+
25
+ state = matching_state(rm_value)
26
+ return false if state.nil?
27
+ return true unless rm_value.respond_to?(:is_terminal?)
28
+
29
+ rm_value.is_terminal? == state.is_a?(TerminalState)
30
+ end
31
+
32
+ private
33
+
34
+ def matching_state(rm_value)
35
+ return nil unless rm_value.respond_to?(:value)
36
+
37
+ code = state_code(rm_value.value)
38
+ return nil if code.nil?
39
+
40
+ value.states.find { |s| s.name == code }
41
+ end
42
+
43
+ def state_code(raw)
44
+ return raw if raw.is_a?(String)
45
+ return raw.code_string if raw.respond_to?(:code_string)
46
+
47
+ nil
48
+ end
21
49
  end
22
50
 
23
51
  class StateMachine
@@ -21,6 +21,13 @@ module OpenEHR
21
21
  return false
22
22
  end
23
23
  end
24
+
25
+ def valid_value?(value)
26
+ return true if any_allowed?
27
+ return false if list.nil? || value.nil?
28
+
29
+ list.any? { |item| item.matches?(value) }
30
+ end
24
31
  end
25
32
 
26
33
  class CDvOrdinal < OpenEHR::AM::Archetype::ConstraintModel::CDomainType
@@ -33,6 +40,63 @@ module OpenEHR
33
40
  def any_allowed?
34
41
  @list.nil?
35
42
  end
43
+
44
+ def valid_value?(value)
45
+ return true if any_allowed?
46
+ return false unless value.respond_to?(:value) && value.respond_to?(:symbol)
47
+
48
+ list.any? do |item|
49
+ item.value == value.value && symbol_code(item.symbol) == symbol_code(value.symbol)
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ # DV_ORDINAL.symbol is spec'd as DV_CODED_TEXT, which only
56
+ # exposes a code via defining_code.code_string (DvCodedText
57
+ # itself has no code_string of its own) - but also accept a
58
+ # bare CODE_PHRASE-like symbol (direct code_string) since
59
+ # that's simpler and sufficient for many callers.
60
+ def symbol_code(symbol)
61
+ return symbol.code_string if symbol.respond_to?(:code_string)
62
+ return symbol.defining_code.code_string if symbol.respond_to?(:defining_code)
63
+
64
+ nil
65
+ end
66
+ end
67
+
68
+ # RM 1.1.0: the AOM constraint for DV_SCALE, mirroring
69
+ # CDvOrdinal (an enumeration of allowed value/symbol pairs)
70
+ # except DV_SCALE.value is Real rather than Integer.
71
+ class CDvScale < OpenEHR::AM::Archetype::ConstraintModel::CDomainType
72
+ attr_accessor :list
73
+
74
+ def initialize(args = { })
75
+ super
76
+ self.list = args[:list]
77
+ end
78
+
79
+ def any_allowed?
80
+ @list.nil?
81
+ end
82
+
83
+ def valid_value?(value)
84
+ return true if any_allowed?
85
+ return false unless value.respond_to?(:value) && value.respond_to?(:symbol)
86
+
87
+ list.any? do |item|
88
+ item.value == value.value && symbol_code(item.symbol) == symbol_code(value.symbol)
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ def symbol_code(symbol)
95
+ return symbol.code_string if symbol.respond_to?(:code_string)
96
+ return symbol.defining_code.code_string if symbol.respond_to?(:defining_code)
97
+
98
+ nil
99
+ end
36
100
  end
37
101
 
38
102
  class CQuantityItem
@@ -53,12 +117,36 @@ module OpenEHR
53
117
  end
54
118
 
55
119
  def precision_unconstrained?
120
+ return true if @precision.nil?
56
121
  if @precision.upper == -1 && @precision.lower == -1
57
122
  return true
58
123
  else
59
124
  return false
60
125
  end
61
126
  end
127
+
128
+ def matches?(value)
129
+ return false unless value.respond_to?(:units) && value.units == units
130
+
131
+ magnitude_matches?(value.magnitude) &&
132
+ precision_matches?(value.respond_to?(:precision) ? value.precision : nil)
133
+ end
134
+
135
+ private
136
+
137
+ def magnitude_matches?(value_magnitude)
138
+ return true if magnitude.nil?
139
+ return magnitude.has?(value_magnitude) if magnitude.respond_to?(:has?)
140
+
141
+ magnitude == value_magnitude
142
+ end
143
+
144
+ def precision_matches?(value_precision)
145
+ return true if precision_unconstrained?
146
+ return precision.has?(value_precision) if precision.respond_to?(:has?)
147
+
148
+ precision == value_precision
149
+ end
62
150
  end
63
151
  end
64
152
  end # of DataTypes
@@ -18,6 +18,29 @@ module OpenEHR
18
18
  def any_allowed?
19
19
  @terminology_id.nil? && @code_list.nil?
20
20
  end
21
+
22
+ def valid_value?(value)
23
+ return true if any_allowed?
24
+ return false if value.nil?
25
+
26
+ code, term = code_and_terminology(value)
27
+ return false if code.nil?
28
+
29
+ (code_list.nil? || code_list.include?(code)) &&
30
+ (terminology_id.nil? || term.nil? || term == terminology_id.value)
31
+ end
32
+
33
+ private
34
+
35
+ def code_and_terminology(value)
36
+ if value.is_a?(String)
37
+ [value, nil]
38
+ elsif value.respond_to?(:code_string)
39
+ [value.code_string, value.respond_to?(:terminology_id) ? value.terminology_id&.value : nil]
40
+ else
41
+ [nil, nil]
42
+ end
43
+ end
21
44
  end
22
45
  end # of Text
23
46
  end # of Data_Types
@@ -11,13 +11,19 @@ module OpenEHR
11
11
  attr_reader :component_terminologies, :terminology_extracts, :template_id
12
12
 
13
13
  def initialize(args = {})
14
+ # template_id must be validated before delegating to Archetype#initialize:
15
+ # OPERATIONAL_TEMPLATE identifies itself by template_id, and archetype_id
16
+ # is derived from it below, so an absent template_id must fail with a
17
+ # template-specific message rather than falling through to Archetype's
18
+ # own (unrelated) mandatory-field checks.
19
+ self.template_id = args[:template_id]
20
+
14
21
  # Initialize parent archetype with template-specific archetype_id
15
22
  template_args = args.dup
16
23
  template_args[:archetype_id] = args[:template_id] if args[:template_id] && !args[:archetype_id]
17
-
24
+
18
25
  super(template_args)
19
-
20
- self.template_id = args[:template_id]
26
+
21
27
  self.component_terminologies = args[:component_terminologies] || {}
22
28
  self.terminology_extracts = args[:terminology_extracts] || {}
23
29
  end
@@ -31,6 +37,22 @@ module OpenEHR
31
37
  @archetype_id = template_id if template_id
32
38
  end
33
39
 
40
+ # An operational template's concept is commonly derived from its
41
+ # archetype_id (see #concept below) rather than supplied explicitly,
42
+ # so unlike Archetype#concept= it must not require a value up front.
43
+ def concept=(concept)
44
+ @concept = concept
45
+ end
46
+
47
+ # AuthoredResource#description has no mandatory check, but an
48
+ # operational template always carries its own resource description.
49
+ def description=(description)
50
+ if description.nil?
51
+ raise ArgumentError, 'description is mandatory'
52
+ end
53
+ @description = description
54
+ end
55
+
34
56
  def component_terminologies=(component_terminologies)
35
57
  @component_terminologies = component_terminologies || {}
36
58
  end
@@ -0,0 +1,13 @@
1
+ module OpenEHR
2
+ module AQL
3
+ # A single row-candidate produced while walking the FROM clause's
4
+ # containment tree: the Dataset::EHRRecord it came from (for "e/..."
5
+ # root paths) plus the RM objects bound to each FROM variable so far
6
+ # (e.g. {"c" => a_composition, "o" => an_observation}).
7
+ Binding = Struct.new(:ehr_record, :variables, keyword_init: true) do
8
+ def [](name)
9
+ variables.fetch(name) { raise SemanticError, "unbound variable: #{name.inspect}" }
10
+ end
11
+ end
12
+ end
13
+ end