openehr 1.2.999999 → 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 +95 -14
- 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 +77 -24
- 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 +306 -27
- data/lib/openehr/parser.rb +2 -0
- data/lib/openehr/path.rb +195 -0
- data/lib/openehr/rm/common/archetyped.rb +117 -7
- 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/data_types/uri.rb +0 -1
- data/lib/openehr/rm/demographic.rb +4 -3
- data/lib/openehr/rm/ehr.rb +20 -1
- data/lib/openehr/rm/factory.rb +345 -36
- 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 +36 -11
- 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
|
@@ -39,15 +39,42 @@ module OpenEHR
|
|
|
39
39
|
def parse
|
|
40
40
|
@opt = Nokogiri::XML::Document.parse(File.open(@filename))
|
|
41
41
|
@opt.remove_namespaces!
|
|
42
|
-
|
|
42
|
+
|
|
43
|
+
uid = build_uid
|
|
43
44
|
defs = definition
|
|
44
|
-
|
|
45
|
+
|
|
46
|
+
# Create operational template with archetype-compatible parameters
|
|
47
|
+
OpenEHR::AM::Template::OperationalTemplate.new(
|
|
48
|
+
uid: uid,
|
|
49
|
+
concept: concept,
|
|
50
|
+
original_language: language,
|
|
51
|
+
description: description,
|
|
52
|
+
template_id: template_id,
|
|
53
|
+
archetype_id: template_id, # Use template_id as archetype_id for compatibility
|
|
54
|
+
definition: defs,
|
|
55
|
+
ontology: (@component_terminologies || {})[defs.archetype_id.value] || create_template_ontology,
|
|
56
|
+
component_terminologies: @component_terminologies || {},
|
|
57
|
+
terminology_extracts: @component_terminologies || {},
|
|
58
|
+
adl_version: "1.4"
|
|
59
|
+
)
|
|
45
60
|
end
|
|
46
61
|
|
|
47
62
|
private
|
|
48
63
|
|
|
64
|
+
# template_id is mandatory for an operational template (enforced by
|
|
65
|
+
# OperationalTemplate itself); a missing/blank <template_id> element
|
|
66
|
+
# must therefore resolve to nil rather than an invalid TemplateID.
|
|
49
67
|
def template_id
|
|
50
|
-
@template_id
|
|
68
|
+
return @template_id if @template_id
|
|
69
|
+
value = text_on_path(@opt, TEMPLATE_ID_PATH)
|
|
70
|
+
@template_id = value.nil? || value.empty? ? nil : OpenEHR::RM::Support::Identification::TemplateID.new(value: value)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# uid is optional on an operational template; a missing/blank <uid>
|
|
74
|
+
# element must resolve to nil rather than an invalid UIDBasedID.
|
|
75
|
+
def build_uid
|
|
76
|
+
value = text_on_path(@opt, UID_PATH)
|
|
77
|
+
value.nil? || value.empty? ? nil : OpenEHR::RM::Support::Identification::UIDBasedID.new(value: value)
|
|
51
78
|
end
|
|
52
79
|
|
|
53
80
|
def concept
|
|
@@ -92,6 +119,30 @@ module OpenEHR
|
|
|
92
119
|
archetype_terminology(nodes)
|
|
93
120
|
end
|
|
94
121
|
|
|
122
|
+
def create_template_ontology
|
|
123
|
+
# Create a basic ontology for the template using the main concept
|
|
124
|
+
concept_code = 'at0000'
|
|
125
|
+
original_lang = language
|
|
126
|
+
|
|
127
|
+
term_definitions = {
|
|
128
|
+
original_lang.code_string => [
|
|
129
|
+
OpenEHR::AM::Archetype::Terminology::ArchetypeTerm.new(
|
|
130
|
+
code: concept_code,
|
|
131
|
+
items: {
|
|
132
|
+
'text' => concept || 'Template',
|
|
133
|
+
'description' => 'Operational template'
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
OpenEHR::AM::Archetype::Terminology::ArchetypeTerminology.new(
|
|
140
|
+
concept_code: concept_code,
|
|
141
|
+
original_language: original_lang,
|
|
142
|
+
term_definitions: term_definitions
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
|
|
95
146
|
def archetype_terminology(nodes)
|
|
96
147
|
td = term_definitions(nodes)
|
|
97
148
|
concept_code = td[language.code_string][0]
|
|
@@ -174,10 +225,23 @@ module OpenEHR
|
|
|
174
225
|
end
|
|
175
226
|
|
|
176
227
|
def c_code_phrase(attr_xml, node)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
228
|
+
terminology_id_node = attr_xml.at('terminology_id/value')
|
|
229
|
+
terminology_id = terminology_id_node ? OpenEHR::RM::Support::Identification::TerminologyID.new(value: terminology_id_node.text.strip) : nil
|
|
230
|
+
|
|
231
|
+
code_list_nodes = attr_xml.xpath('code_list')
|
|
232
|
+
code_list = code_list_nodes.map { |code_node| code_node.text.strip }
|
|
233
|
+
code_list = [code_list.first] if code_list.size == 1 && code_list.first.empty?
|
|
234
|
+
|
|
235
|
+
occurrences_node = attr_xml.at('occurrences')
|
|
236
|
+
occurrences_obj = occurrences_node ? occurrences(occurrences_node) : nil
|
|
237
|
+
|
|
238
|
+
OpenEHR::AM::OpenEHRProfile::DataTypes::Text::CCodePhrase.new(
|
|
239
|
+
terminology_id: terminology_id,
|
|
240
|
+
code_list: code_list,
|
|
241
|
+
path: node.path,
|
|
242
|
+
occurrences: occurrences_obj,
|
|
243
|
+
rm_type_name: 'CODE_PHRASE'
|
|
244
|
+
)
|
|
181
245
|
end
|
|
182
246
|
|
|
183
247
|
def archetype_slot(attr_xml,node)
|
|
@@ -193,22 +257,82 @@ module OpenEHR
|
|
|
193
257
|
end
|
|
194
258
|
|
|
195
259
|
def occurrences(occurrence_xml)
|
|
260
|
+
return nil if occurrence_xml.nil?
|
|
261
|
+
|
|
196
262
|
lower_node = occurrence_xml.at('lower')
|
|
197
263
|
upper_node = occurrence_xml.at('upper')
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
264
|
+
lower_included_node = occurrence_xml.at('lower_included')
|
|
265
|
+
upper_included_node = occurrence_xml.at('upper_included')
|
|
266
|
+
lower_unbounded_node = occurrence_xml.at('lower_unbounded')
|
|
267
|
+
upper_unbounded_node = occurrence_xml.at('upper_unbounded')
|
|
268
|
+
|
|
269
|
+
lower = lower_node ? lower_node.text.to_i : nil
|
|
270
|
+
upper = upper_node ? upper_node.text.to_i : nil
|
|
271
|
+
lower_included = lower_included_node ? to_bool(lower_included_node.text) : (lower.nil? ? nil : true)
|
|
272
|
+
upper_included = upper_included_node ? to_bool(upper_included_node.text) : (upper.nil? ? nil : true)
|
|
273
|
+
lower_unbounded = lower_unbounded_node ? to_bool(lower_unbounded_node.text) : false
|
|
274
|
+
upper_unbounded = upper_unbounded_node ? to_bool(upper_unbounded_node.text) : false
|
|
275
|
+
|
|
276
|
+
# An occurrences element with none of its children present carries no
|
|
277
|
+
# constraint at all; Interval requires at least one bound, so treat
|
|
278
|
+
# this as "no occurrences data" rather than raising.
|
|
279
|
+
return nil if lower.nil? && upper.nil? && !lower_unbounded && !upper_unbounded
|
|
280
|
+
|
|
281
|
+
# Handle unbounded intervals properly
|
|
282
|
+
if upper_unbounded || upper.nil?
|
|
283
|
+
upper = nil
|
|
284
|
+
upper_included = nil
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
if lower_unbounded || lower.nil?
|
|
288
|
+
lower = nil
|
|
289
|
+
lower_included = nil
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
OpenEHR::AssumedLibraryTypes::Interval.new(
|
|
293
|
+
lower: lower,
|
|
294
|
+
upper: upper,
|
|
295
|
+
lower_included: lower_included,
|
|
296
|
+
upper_included: upper_included
|
|
297
|
+
)
|
|
203
298
|
end
|
|
204
299
|
|
|
205
300
|
def cardinality(xml)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
301
|
+
return nil if xml.nil?
|
|
302
|
+
|
|
303
|
+
order_node = xml.at('is_ordered')
|
|
304
|
+
unique_node = xml.at('is_unique')
|
|
305
|
+
interval_node = xml.at('interval')
|
|
306
|
+
|
|
307
|
+
# No cardinality sub-elements at all means no cardinality data.
|
|
308
|
+
return nil if order_node.nil? && unique_node.nil? && interval_node.nil?
|
|
309
|
+
|
|
310
|
+
order = order_node ? to_bool(order_node.text) : false
|
|
311
|
+
unique = unique_node ? to_bool(unique_node.text) : false
|
|
312
|
+
interval = interval_node ? occurrences(interval_node) : nil
|
|
313
|
+
|
|
314
|
+
OpenEHR::AM::Archetype::ConstraintModel::Cardinality.new(
|
|
315
|
+
is_ordered: order,
|
|
316
|
+
is_unique: unique,
|
|
317
|
+
interval: interval
|
|
318
|
+
)
|
|
210
319
|
end
|
|
211
320
|
|
|
321
|
+
def archetype_internal_ref(attr_xml, node)
|
|
322
|
+
rm_type_name = attr_xml.at('rm_type_name').text
|
|
323
|
+
target_path = attr_xml.at('target_path').text
|
|
324
|
+
occurrences = occurrences(attr_xml.at('occurrences'))
|
|
325
|
+
OpenEHR::AM::Archetype::ConstraintModel::ArchetypeInternalRef.new(rm_type_name: rm_type_name, occurrences: occurrences, target_path: target_path)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# No .opt fixture in this gem's corpus uses a C_DV_STATE (state
|
|
329
|
+
# machine) constraint, so its actual OPT XML shape is unverified;
|
|
330
|
+
# raising a clear, documented error here is safer than guessing
|
|
331
|
+
# at element names and risking a silently wrong StateMachine.
|
|
332
|
+
def c_dv_state(_attr_xml, _node)
|
|
333
|
+
raise NotImplementedError, 'OPTParser does not yet support C_DV_STATE (state machine) constraints'
|
|
334
|
+
end
|
|
335
|
+
|
|
212
336
|
def constraint_ref(attr_xml, node)
|
|
213
337
|
rm_type_name = attr_xml.at('rm_type_name').text
|
|
214
338
|
reference = attr_xml.at('reference').text
|
|
@@ -265,9 +389,7 @@ module OpenEHR
|
|
|
265
389
|
def c_dv_quantity(attr_xml, node)
|
|
266
390
|
rm_type_name = attr_xml.at('rm_type_name').text
|
|
267
391
|
occurrences = occurrences(attr_xml.at('occurrences'))
|
|
268
|
-
|
|
269
|
-
property_code_string = attr_xml.at('property/code_string').text
|
|
270
|
-
property = OpenEHR::RM::DataTypes::Text::CodePhrase.new(terminology_id: property_terminology_id, code_string: property_code_string)
|
|
392
|
+
property = property_code_phrase(attr_xml.at('property'))
|
|
271
393
|
list = attr_xml.xpath('.//list').map do |element|
|
|
272
394
|
units = element.at('units').text if element.at('units')
|
|
273
395
|
magnitude = occurrences(element.at('magnitude')) if element.at('magnitude')
|
|
@@ -277,19 +399,177 @@ module OpenEHR
|
|
|
277
399
|
OpenEHR::AM::OpenEHRProfile::DataTypes::Quantity::CDvQuantity.new(rm_type_name: rm_type_name, occurrences: occurrences, list: list, property: property)
|
|
278
400
|
end
|
|
279
401
|
|
|
280
|
-
|
|
402
|
+
# The <property> element is optional in real templates; return nil rather
|
|
403
|
+
# than dereferencing missing terminology/code nodes.
|
|
404
|
+
def property_code_phrase(property_xml)
|
|
405
|
+
return nil if property_xml.nil?
|
|
406
|
+
terminology_node = property_xml.at('terminology_id/value')
|
|
407
|
+
code_node = property_xml.at('code_string')
|
|
408
|
+
return nil if terminology_node.nil? || code_node.nil?
|
|
409
|
+
terminology_id = OpenEHR::RM::Support::Identification::TerminologyID.new(value: terminology_node.text)
|
|
410
|
+
OpenEHR::RM::DataTypes::Text::CodePhrase.new(terminology_id: terminology_id, code_string: code_node.text)
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def c_dv_ordinal(attr_xml, node)
|
|
414
|
+
rm_type_name = attr_xml.at('rm_type_name').text
|
|
415
|
+
occurrences = occurrences(attr_xml.at('occurrences'))
|
|
416
|
+
list = attr_xml.xpath('list').map { |element| dv_ordinal_item(element) }.compact
|
|
417
|
+
OpenEHR::AM::OpenEHRProfile::DataTypes::Quantity::CDvOrdinal.new(rm_type_name: rm_type_name, occurrences: occurrences, list: list)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# DV_ORDINAL.symbol is spec'd as DV_CODED_TEXT; the OPT XML only
|
|
421
|
+
# carries a defining_code (terminology_id + code_string), so the
|
|
422
|
+
# DvCodedText's own value is set to that same code_string (there
|
|
423
|
+
# is no separate display text in this element).
|
|
424
|
+
def dv_ordinal_item(element)
|
|
425
|
+
value_node = element.at('value')
|
|
426
|
+
return nil unless value_node && !value_node.text.empty?
|
|
427
|
+
|
|
428
|
+
code_phrase = property_code_phrase(element.at('symbol/defining_code'))
|
|
429
|
+
return nil if code_phrase.nil?
|
|
430
|
+
|
|
431
|
+
symbol = OpenEHR::RM::DataTypes::Text::DvCodedText.new(value: code_phrase.code_string, defining_code: code_phrase)
|
|
432
|
+
OpenEHR::RM::DataTypes::Quantity::DvOrdinal.new(value: value_node.text.to_i, symbol: symbol)
|
|
433
|
+
end
|
|
281
434
|
|
|
435
|
+
def c_dv_scale(attr_xml, node)
|
|
436
|
+
rm_type_name = attr_xml.at('rm_type_name').text
|
|
437
|
+
occurrences = occurrences(attr_xml.at('occurrences'))
|
|
438
|
+
list = attr_xml.xpath('list').map { |element| dv_scale_item(element) }.compact
|
|
439
|
+
OpenEHR::AM::OpenEHRProfile::DataTypes::Quantity::CDvScale.new(rm_type_name: rm_type_name, occurrences: occurrences, list: list)
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# Same XML shape as C_DV_ORDINAL's list items, but DV_SCALE.value
|
|
443
|
+
# is Real rather than Integer.
|
|
444
|
+
def dv_scale_item(element)
|
|
445
|
+
value_node = element.at('value')
|
|
446
|
+
return nil unless value_node && !value_node.text.empty?
|
|
447
|
+
|
|
448
|
+
code_phrase = property_code_phrase(element.at('symbol/defining_code'))
|
|
449
|
+
return nil if code_phrase.nil?
|
|
450
|
+
|
|
451
|
+
symbol = OpenEHR::RM::DataTypes::Text::DvCodedText.new(value: code_phrase.code_string, defining_code: code_phrase)
|
|
452
|
+
OpenEHR::RM::DataTypes::Quantity::DvScale.new(value: value_node.text.to_f, symbol: symbol)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def c_date(xml)
|
|
456
|
+
pattern = xml.at('pattern')
|
|
457
|
+
range = xml.at('range')
|
|
458
|
+
if pattern
|
|
459
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDate.new(pattern: pattern.text)
|
|
460
|
+
elsif range
|
|
461
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDate.new(range: occurrences(range))
|
|
462
|
+
else
|
|
463
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDate.new
|
|
464
|
+
end
|
|
282
465
|
end
|
|
283
466
|
|
|
284
467
|
def c_date_time(xml)
|
|
285
|
-
|
|
468
|
+
pattern = xml.at('pattern')
|
|
469
|
+
range = xml.at('range')
|
|
470
|
+
if pattern
|
|
471
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDateTime.new(pattern: pattern.text)
|
|
472
|
+
elsif range
|
|
473
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDateTime.new(range: occurrences(range))
|
|
474
|
+
else
|
|
475
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDateTime.new
|
|
476
|
+
end
|
|
286
477
|
end
|
|
287
478
|
|
|
288
479
|
def c_integer(xml)
|
|
480
|
+
range = xml.at('range')
|
|
481
|
+
list = xml.xpath('list')
|
|
482
|
+
if range
|
|
483
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CInteger.new(range: occurrences(range))
|
|
484
|
+
elsif !list.empty?
|
|
485
|
+
list_values = list.map { |item| item.text.to_i }
|
|
486
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CInteger.new(list: list_values)
|
|
487
|
+
else
|
|
488
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CInteger.new
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def c_real(xml)
|
|
493
|
+
range = xml.at('range')
|
|
494
|
+
list = xml.xpath('list')
|
|
495
|
+
if range
|
|
496
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CReal.new(range: occurrences(range))
|
|
497
|
+
elsif !list.empty?
|
|
498
|
+
list_values = list.map { |item| item.text.to_f }
|
|
499
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CReal.new(list: list_values)
|
|
500
|
+
else
|
|
501
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CReal.new
|
|
502
|
+
end
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def c_duration(xml)
|
|
506
|
+
pattern = xml.at('pattern')
|
|
507
|
+
range_xml = xml.at('range')
|
|
508
|
+
list = xml.xpath('list')
|
|
509
|
+
if pattern
|
|
510
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDuration.new(pattern: pattern.text)
|
|
511
|
+
elsif range_xml
|
|
512
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDuration.new(range: duration_range(range_xml))
|
|
513
|
+
elsif !list.empty?
|
|
514
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDuration.new(list: list.map(&:text))
|
|
515
|
+
else
|
|
516
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CDuration.new
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# A C_DURATION range's bounds are ISO8601 duration strings (e.g.
|
|
521
|
+
# PT24H), not plain numbers, so occurrences() (built for numeric
|
|
522
|
+
# Interval bounds) doesn't apply here; wrap each bound as a
|
|
523
|
+
# DV_DURATION instead, matching what CDuration#valid_value?
|
|
524
|
+
# already expects its range bounds to be.
|
|
525
|
+
def duration_range(range_xml)
|
|
526
|
+
lower = duration_bound(range_xml.at('lower'), range_xml.at('lower_unbounded'))
|
|
527
|
+
upper = duration_bound(range_xml.at('upper'), range_xml.at('upper_unbounded'))
|
|
528
|
+
return nil if lower.nil? && upper.nil?
|
|
529
|
+
|
|
530
|
+
OpenEHR::AssumedLibraryTypes::Interval.new(
|
|
531
|
+
lower: lower, upper: upper,
|
|
532
|
+
lower_included: lower.nil? ? nil : bool_node(range_xml.at('lower_included'), true),
|
|
533
|
+
upper_included: upper.nil? ? nil : bool_node(range_xml.at('upper_included'), true))
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def duration_bound(value_node, unbounded_node)
|
|
537
|
+
return nil if bool_node(unbounded_node, false)
|
|
538
|
+
return nil if value_node.nil? || value_node.text.empty?
|
|
289
539
|
|
|
540
|
+
OpenEHR::RM::DataTypes::Quantity::DateTime::DvDuration.new(value: value_node.text)
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def bool_node(node, default)
|
|
544
|
+
node ? to_bool(node.text) : default
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def c_time(xml)
|
|
548
|
+
pattern = xml.at('pattern')
|
|
549
|
+
range = xml.at('range')
|
|
550
|
+
if pattern
|
|
551
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CTime.new(pattern: pattern.text)
|
|
552
|
+
elsif range
|
|
553
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CTime.new(range: occurrences(range))
|
|
554
|
+
else
|
|
555
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CTime.new
|
|
556
|
+
end
|
|
290
557
|
end
|
|
291
558
|
|
|
292
559
|
def c_boolean(xml)
|
|
560
|
+
true_valid = xml.at('true_valid')
|
|
561
|
+
false_valid = xml.at('false_valid')
|
|
562
|
+
assumed_value = xml.at('assumed_value')
|
|
563
|
+
|
|
564
|
+
true_valid_value = true_valid ? to_bool(true_valid.text) : nil
|
|
565
|
+
false_valid_value = false_valid ? to_bool(false_valid.text) : nil
|
|
566
|
+
assumed_value_value = assumed_value ? to_bool(assumed_value.text) : nil
|
|
567
|
+
|
|
568
|
+
OpenEHR::AM::Archetype::ConstraintModel::Primitive::CBoolean.new(
|
|
569
|
+
true_valid: true_valid_value,
|
|
570
|
+
false_valid: false_valid_value,
|
|
571
|
+
assumed_value: assumed_value_value
|
|
572
|
+
)
|
|
293
573
|
end
|
|
294
574
|
|
|
295
575
|
def string(attr_xml)
|
|
@@ -309,12 +589,11 @@ module OpenEHR
|
|
|
309
589
|
end
|
|
310
590
|
|
|
311
591
|
def to_bool(str)
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
return nil
|
|
592
|
+
return nil if str.nil?
|
|
593
|
+
str = str.text if str.respond_to?(:text)
|
|
594
|
+
return true if /true/i =~ str.to_s
|
|
595
|
+
return false if /false/i =~ str.to_s
|
|
596
|
+
nil
|
|
318
597
|
end
|
|
319
598
|
end
|
|
320
599
|
end
|
data/lib/openehr/parser.rb
CHANGED
data/lib/openehr/path.rb
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# OpenEHR::Path parses the openEHR archetype path syntax used to
|
|
2
|
+
# navigate LOCATABLE/PATHABLE trees, e.g.
|
|
3
|
+
# /content[at0001]/data[at0002]/events[at0006]/data[at0003]/items[at0004, 'Systolic']/value
|
|
4
|
+
#
|
|
5
|
+
# Grammar subset supported (see the plan for the full rationale):
|
|
6
|
+
# path := '/' | ('/' segment)+
|
|
7
|
+
# segment := attribute predicate?
|
|
8
|
+
# attribute := [a-z][a-zA-Z0-9_]*
|
|
9
|
+
# predicate := '[' node_id (',' ws* name)? ']'
|
|
10
|
+
# node_id := at-code (at\d+(\.\d+)*, incl. specialised at0001.1 and
|
|
11
|
+
# the short at0.2 form ADL 1.4 uses for nodes newly
|
|
12
|
+
# introduced by a specialisation)
|
|
13
|
+
# | archetype-id
|
|
14
|
+
# name := "'" ... "'" (a Locatable#name.value literal)
|
|
15
|
+
#
|
|
16
|
+
# Deliberately unsupported (raises InvalidPathError): relative paths,
|
|
17
|
+
# '//' wildcards, numeric index predicates, and general expression
|
|
18
|
+
# predicates. Those are layered on top by consumers (e.g. AQL); this
|
|
19
|
+
# class only parses the path shape used by RM path navigation.
|
|
20
|
+
#
|
|
21
|
+
# This file has no dependency on any RM class, so it can be required
|
|
22
|
+
# standalone.
|
|
23
|
+
module OpenEHR
|
|
24
|
+
class Path
|
|
25
|
+
class InvalidPathError < ArgumentError; end
|
|
26
|
+
|
|
27
|
+
AT_CODE = /\Aat\d+(\.\d+)*\z/
|
|
28
|
+
ARCHETYPE_ID = /\A[a-zA-Z]\w+-[a-zA-Z]\w+-[a-zA-Z]\w+\.[a-zA-Z]\w+(-[a-zA-Z]\w+)?\.v\d+\z/
|
|
29
|
+
ATTRIBUTE = /\A[a-z][a-zA-Z0-9_]*\z/
|
|
30
|
+
|
|
31
|
+
class Segment
|
|
32
|
+
attr_reader :attribute, :archetype_node_id, :name
|
|
33
|
+
|
|
34
|
+
def initialize(attribute, archetype_node_id: nil, name: nil)
|
|
35
|
+
unless attribute.is_a?(String) && attribute =~ ATTRIBUTE
|
|
36
|
+
raise InvalidPathError, "invalid path attribute: #{attribute.inspect}"
|
|
37
|
+
end
|
|
38
|
+
if archetype_node_id && !(archetype_node_id =~ AT_CODE || archetype_node_id =~ ARCHETYPE_ID)
|
|
39
|
+
raise InvalidPathError, "invalid node id: #{archetype_node_id.inspect}"
|
|
40
|
+
end
|
|
41
|
+
raise InvalidPathError, 'name predicate requires a node_id predicate' if name && archetype_node_id.nil?
|
|
42
|
+
|
|
43
|
+
@attribute = attribute
|
|
44
|
+
@archetype_node_id = archetype_node_id
|
|
45
|
+
@name = name
|
|
46
|
+
freeze
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def predicate?
|
|
50
|
+
!@archetype_node_id.nil?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def to_s
|
|
54
|
+
return @attribute unless predicate?
|
|
55
|
+
|
|
56
|
+
s = "#{@attribute}[#{@archetype_node_id}"
|
|
57
|
+
s += ", '#{@name}'" if @name
|
|
58
|
+
s + ']'
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def ==(other)
|
|
62
|
+
other.is_a?(Segment) &&
|
|
63
|
+
attribute == other.attribute &&
|
|
64
|
+
archetype_node_id == other.archetype_node_id &&
|
|
65
|
+
name == other.name
|
|
66
|
+
end
|
|
67
|
+
alias eql? ==
|
|
68
|
+
|
|
69
|
+
def hash
|
|
70
|
+
[attribute, archetype_node_id, name].hash
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.parse(str)
|
|
75
|
+
raise InvalidPathError, 'path must be a String' unless str.is_a?(String)
|
|
76
|
+
raise InvalidPathError, "path must start with '/': #{str.inspect}" unless str.start_with?('/')
|
|
77
|
+
return new([]) if str == '/'
|
|
78
|
+
|
|
79
|
+
body = str[1..-1]
|
|
80
|
+
raise InvalidPathError, "path must not end with '/': #{str.inspect}" if body.end_with?('/')
|
|
81
|
+
|
|
82
|
+
new(split_segments(body).map { |token| parse_segment(token) })
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.valid?(str)
|
|
86
|
+
parse(str)
|
|
87
|
+
true
|
|
88
|
+
rescue InvalidPathError
|
|
89
|
+
false
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Splits on top-level '/' only (not '/' occurring inside a '[...]'
|
|
93
|
+
# predicate), and rejects empty segments (i.e. a '//' wildcard).
|
|
94
|
+
def self.split_segments(body)
|
|
95
|
+
tokens = []
|
|
96
|
+
depth = 0
|
|
97
|
+
current = +''
|
|
98
|
+
body.each_char do |c|
|
|
99
|
+
case c
|
|
100
|
+
when '['
|
|
101
|
+
depth += 1
|
|
102
|
+
current << c
|
|
103
|
+
when ']'
|
|
104
|
+
depth -= 1
|
|
105
|
+
current << c
|
|
106
|
+
when '/'
|
|
107
|
+
if depth.zero?
|
|
108
|
+
raise InvalidPathError, "empty path segment ('//' is not supported): #{body.inspect}" if current.empty?
|
|
109
|
+
|
|
110
|
+
tokens << current
|
|
111
|
+
current = +''
|
|
112
|
+
else
|
|
113
|
+
current << c
|
|
114
|
+
end
|
|
115
|
+
else
|
|
116
|
+
current << c
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
raise InvalidPathError, "unbalanced '[' in path: #{body.inspect}" unless depth.zero?
|
|
120
|
+
raise InvalidPathError, "empty path segment ('//' is not supported): #{body.inspect}" if current.empty?
|
|
121
|
+
|
|
122
|
+
tokens << current
|
|
123
|
+
tokens
|
|
124
|
+
end
|
|
125
|
+
private_class_method :split_segments
|
|
126
|
+
|
|
127
|
+
def self.parse_segment(token)
|
|
128
|
+
if token =~ /\A([^\[\]]+)\[(.*)\]\z/
|
|
129
|
+
attribute = Regexp.last_match(1)
|
|
130
|
+
predicate = Regexp.last_match(2)
|
|
131
|
+
if predicate =~ /\A([^,]+),\s*'([^']*)'\z/
|
|
132
|
+
Segment.new(attribute, archetype_node_id: Regexp.last_match(1).strip, name: Regexp.last_match(2))
|
|
133
|
+
else
|
|
134
|
+
Segment.new(attribute, archetype_node_id: predicate.strip)
|
|
135
|
+
end
|
|
136
|
+
elsif token =~ /\A[^\[\]]+\z/
|
|
137
|
+
Segment.new(token)
|
|
138
|
+
else
|
|
139
|
+
raise InvalidPathError, "malformed path segment: #{token.inspect}"
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
private_class_method :parse_segment
|
|
143
|
+
|
|
144
|
+
attr_reader :segments
|
|
145
|
+
|
|
146
|
+
def initialize(segments)
|
|
147
|
+
@segments = segments.freeze
|
|
148
|
+
freeze
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def root?
|
|
152
|
+
@segments.empty?
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def to_s
|
|
156
|
+
return '/' if root?
|
|
157
|
+
|
|
158
|
+
'/' + @segments.map(&:to_s).join('/')
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def ==(other)
|
|
162
|
+
other.is_a?(Path) && segments == other.segments
|
|
163
|
+
end
|
|
164
|
+
alias eql? ==
|
|
165
|
+
|
|
166
|
+
def hash
|
|
167
|
+
segments.hash
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def parent
|
|
171
|
+
return self if root?
|
|
172
|
+
|
|
173
|
+
self.class.new(segments[0..-2])
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def +(other)
|
|
177
|
+
case other
|
|
178
|
+
when Segment
|
|
179
|
+
self.class.new(segments + [other])
|
|
180
|
+
when Path
|
|
181
|
+
self.class.new(segments + other.segments)
|
|
182
|
+
when String
|
|
183
|
+
self.class.new(segments + [Segment.new(other)])
|
|
184
|
+
else
|
|
185
|
+
raise ArgumentError, "cannot append #{other.class} to a Path"
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def descend
|
|
190
|
+
return [nil, self] if root?
|
|
191
|
+
|
|
192
|
+
[segments.first, self.class.new(segments[1..-1])]
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|