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.
- checksums.yaml +4 -4
- data/README.rdoc +93 -12
- data/lib/openehr/am/archetype/constraint_model/primitive.rb +204 -6
- data/lib/openehr/am/archetype/constraint_model.rb +228 -4
- data/lib/openehr/am/archetype/ontology.rb +2 -2
- data/lib/openehr/am/archetype.rb +101 -2
- data/lib/openehr/am/openehr_profile/data_types/basic.rb +28 -0
- data/lib/openehr/am/openehr_profile/data_types/quantity.rb +88 -0
- data/lib/openehr/am/openehr_profile/data_types/text.rb +23 -0
- data/lib/openehr/am/template.rb +25 -3
- data/lib/openehr/aql/engine/binding.rb +13 -0
- data/lib/openehr/aql/engine/contains_resolver.rb +161 -0
- data/lib/openehr/aql/engine/dataset.rb +122 -0
- data/lib/openehr/aql/engine/path_evaluator.rb +137 -0
- data/lib/openehr/aql/engine/predicate_evaluator.rb +65 -0
- data/lib/openehr/aql/engine.rb +119 -0
- data/lib/openehr/aql/errors.rb +27 -0
- data/lib/openehr/aql/lexer.rb +295 -0
- data/lib/openehr/aql/model/containment.rb +45 -0
- data/lib/openehr/aql/model/from_clause.rb +35 -0
- data/lib/openehr/aql/model/function_call.rb +56 -0
- data/lib/openehr/aql/model/identified_path.rb +20 -0
- data/lib/openehr/aql/model/literal.rb +15 -0
- data/lib/openehr/aql/model/object_path.rb +34 -0
- data/lib/openehr/aql/model/order_by_and_limit.rb +50 -0
- data/lib/openehr/aql/model/predicate.rb +84 -0
- data/lib/openehr/aql/model/query.rb +24 -0
- data/lib/openehr/aql/model/select_clause.rb +29 -0
- data/lib/openehr/aql/model/where_clause.rb +105 -0
- data/lib/openehr/aql/model.rb +14 -0
- data/lib/openehr/aql/parser.rb +484 -0
- data/lib/openehr/aql/result_set.rb +54 -0
- data/lib/openehr/aql.rb +28 -0
- data/lib/openehr/assumed_library_types.rb +68 -17
- data/lib/openehr/parser/adl_grammar.tt +29 -13
- data/lib/openehr/parser/adl_parser.rb +18 -4
- data/lib/openehr/parser/archetype_validator.rb +126 -0
- data/lib/openehr/parser/exception.rb +13 -0
- data/lib/openehr/parser/opt_parser.rb +162 -9
- data/lib/openehr/parser.rb +2 -0
- data/lib/openehr/path.rb +195 -0
- data/lib/openehr/rm/common/archetyped.rb +116 -6
- data/lib/openehr/rm/common/change_control.rb +3 -8
- data/lib/openehr/rm/common/directory.rb +4 -0
- data/lib/openehr/rm/common/generic.rb +24 -10
- data/lib/openehr/rm/composition/content/entry.rb +24 -5
- data/lib/openehr/rm/composition/content/navigation.rb +2 -1
- data/lib/openehr/rm/composition.rb +19 -2
- data/lib/openehr/rm/data_structures/history.rb +3 -0
- data/lib/openehr/rm/data_structures/item_structure/representation.rb +13 -9
- data/lib/openehr/rm/data_structures/item_structure.rb +28 -15
- data/lib/openehr/rm/data_types/encapsulated.rb +23 -3
- data/lib/openehr/rm/data_types/quantity/date_time.rb +9 -1
- data/lib/openehr/rm/data_types/quantity.rb +66 -14
- data/lib/openehr/rm/data_types/text.rb +8 -0
- data/lib/openehr/rm/data_types/time_specification.rb +5 -4
- data/lib/openehr/rm/demographic.rb +4 -3
- data/lib/openehr/rm/ehr.rb +20 -1
- data/lib/openehr/rm/factory.rb +234 -6
- data/lib/openehr/rm/integration.rb +1 -0
- data/lib/openehr/rm/support/identification.rb +37 -8
- data/lib/openehr/rm/support/measurement.rb +32 -0
- data/lib/openehr/rm/type_name.rb +90 -0
- data/lib/openehr/rm.rb +3 -0
- data/lib/openehr/serializer/adl_serializer.rb +335 -0
- data/lib/openehr/serializer/base.rb +20 -0
- data/lib/openehr/serializer/opt_serializer.rb +49 -0
- data/lib/openehr/serializer/rm_json_serializer.rb +62 -0
- data/lib/openehr/serializer/xml_serializer.rb +260 -0
- data/lib/openehr/serializer.rb +5 -291
- data/lib/openehr/terminology_service.rb +44 -0
- data/lib/openehr/version.rb +1 -1
- data/lib/openehr.rb +5 -2
- metadata +35 -7
- data/lib/openehr/parser/validator.rb +0 -18
- data/lib/openehr/parser/xml_parser.rb +0 -13
- data/lib/openehr/rm/data_types/charset_extract.rb +0 -24
- 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
|
-
|
|
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
|
data/lib/openehr/am/archetype.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
data/lib/openehr/am/template.rb
CHANGED
|
@@ -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
|