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
@@ -0,0 +1,335 @@
1
+ require_relative 'base'
2
+
3
+ module OpenEHR
4
+ module Serializer
5
+ class ADLSerializer < BaseSerializer
6
+ def header
7
+ hd = 'archetype'
8
+ unless @archetype.adl_version.nil?
9
+ hd << " (adl_version = #{@archetype.adl_version})"
10
+ end
11
+ hd << NL+INDENT + "#{@archetype.archetype_id.value}"+NL*2
12
+ hd << 'concept'+NL+ INDENT+"[#{@archetype.concept}]"+NL
13
+ hd << NL+'language'+NL+INDENT+'original_language = <['+
14
+ @archetype.original_language.terminology_id.value+'::'+
15
+ @archetype.original_language.code_string+']>'+NL
16
+ return hd
17
+ end
18
+
19
+ def description
20
+ desc = ''
21
+ if @archetype.description
22
+ ad = @archetype.description
23
+ desc << 'description' + NL
24
+ desc << INDENT + 'original_author = <' + NL
25
+ ad.original_author.each do |k,v|
26
+ desc << INDENT+INDENT+'["'+k+'"] = <"'+v+'">'+NL
27
+ end
28
+ desc << INDENT+'>'+NL
29
+ desc << INDENT+'lifecycle_state = <"'+ad.lifecycle_state+'">'+NL
30
+ desc << INDENT+'details = <'+NL
31
+ ad.details.each do |lang,item|
32
+ desc << INDENT*2+'["'+lang+'"] = <'+NL
33
+ desc << INDENT*3+'language = <['+
34
+ item.language.terminology_id.value+'::'+
35
+ item.language.code_string+']>'+NL
36
+ desc << INDENT*3+'purpose = <"'+item.purpose+'">'+NL
37
+ if item.keywords then
38
+ desc << INDENT*3+'keywords = <'
39
+ item.keywords.each do |word|
40
+ desc << '"'+word+'",'
41
+ end
42
+ desc.chop! << '>'+NL
43
+ end
44
+ desc << INDENT*3+'use = <"'+item.use+'">'+NL if item.use
45
+ desc << INDENT*3+'misuse = <"'+item.misuse+'">'+NL if item.misuse
46
+ desc << INDENT*3+'copyright = <"'+item.copyright+'">'+NL if item.copyright
47
+ if item.original_resource_uri
48
+ desc << INDENT*3 + 'original_resource_uri = <'
49
+ item.original_resource_uri.each do |k,v|
50
+ desc << INDENT*4+'["'+k+'"] = <"'+v+'">'+NL
51
+ end
52
+ desc << INDENT*3+'>'+NL
53
+ end
54
+ if item.other_details
55
+ desc << INDENT*3 + 'other_details = <'
56
+ item.other_details.each do |k,v|
57
+ desc << INDENT*4+'["'+k+'"] = <"'+v+'">'+NL
58
+ end
59
+ desc << INDENT*3+'>'+NL
60
+ end
61
+ desc << INDENT*2+'>'+NL
62
+ end
63
+ desc << INDENT+'>'+NL
64
+ end
65
+ return desc
66
+ end
67
+
68
+ def definition
69
+ 'definition' + NL + emit_c_object(@archetype.definition, 1)
70
+ end
71
+
72
+ def ontology
73
+ ao = @archetype.ontology
74
+ ontology = 'ontology'+NL
75
+ ontology << string_list_line('languages_available', ao.languages_available)
76
+ ontology << string_list_line('terminologies_available', ao.terminologies_available)
77
+ ontology << INDENT + 'term_definitions = <' + NL
78
+ ao.term_definitions.each do |lang, items|
79
+ ontology << INDENT*2 + "[\"#{lang}\"] = <" + NL
80
+ ontology << INDENT*3 + 'items = <' + NL
81
+ items.each do |code, item|
82
+ ontology << INDENT*4 + "[\"#{code}\"] = <" + NL
83
+ item.items.each do |name, desc|
84
+ ontology << INDENT*5 + "#{name} = <\"#{desc}\">" +NL
85
+ end
86
+ ontology << INDENT*4 + '>'+NL
87
+ end
88
+ ontology << INDENT*3 + '>' + NL
89
+ ontology << INDENT*2 + '>' + NL
90
+ end
91
+ ontology << INDENT + '>' + NL
92
+ ontology << term_bindings_block(ao.term_bindings)
93
+ ontology
94
+ end
95
+
96
+ def merge
97
+ return header + NL + description + NL + definition + NL + ontology
98
+ end
99
+
100
+ include OpenEHR::AM::Archetype::ConstraintModel
101
+ Primitive = OpenEHR::AM::Archetype::ConstraintModel::Primitive
102
+ OpenEHRProfile = OpenEHR::AM::OpenEHRProfile
103
+
104
+ private
105
+
106
+ def string_list_line(keyword, values)
107
+ return '' if values.nil? || values.empty?
108
+
109
+ INDENT + "#{keyword} = <" + values.map { |v| "\"#{v}\"" }.join(', ') + '>' + NL
110
+ end
111
+
112
+ def term_bindings_block(term_bindings)
113
+ return '' if term_bindings.nil? || term_bindings.empty?
114
+
115
+ block = INDENT + 'term_bindings = <' + NL
116
+ term_bindings.each do |terminology, codes|
117
+ block << INDENT*2 + "[\"#{terminology}\"] = <" + NL
118
+ block << INDENT*3 + 'items = <' + NL
119
+ codes.each do |code, bindings|
120
+ code_phrase = Array(bindings).first
121
+ block << INDENT*4 + "[\"#{code}\"] = <[#{code_phrase.terminology_id.value}::#{code_phrase.code_string}]>" + NL
122
+ end
123
+ block << INDENT*3 + '>' + NL
124
+ block << INDENT*2 + '>' + NL
125
+ end
126
+ block << INDENT + '>' + NL
127
+ end
128
+
129
+ # Recursive cADL emitter for a single C_OBJECT node, dispatching
130
+ # on its concrete class. Unsupported domain types (e.g.
131
+ # C_DV_QUANTITY's dADL block syntax, C_DV_STATE) deliberately
132
+ # raise rather than emit a guess at their syntax.
133
+ def emit_c_object(node, depth)
134
+ case node
135
+ when ArchetypeSlot
136
+ emit_archetype_slot(node, depth)
137
+ when ArchetypeInternalRef
138
+ emit_internal_ref(node, depth)
139
+ when ConstraintRef
140
+ emit_constraint_ref(node, depth)
141
+ when CComplexObject
142
+ emit_complex_object(node, depth)
143
+ else
144
+ raise ArgumentError, "ADLSerializer cannot emit a #{node.class} node"
145
+ end
146
+ end
147
+
148
+ def emit_complex_object(node, depth)
149
+ head = complex_object_head(node, depth)
150
+ if node.any_allowed?
151
+ head + ' matches {*}' + NL
152
+ else
153
+ body = node.attributes.map { |a| emit_c_attribute(a, depth + 1) }.join
154
+ head + ' matches {' + NL + body + INDENT*depth + '}' + NL
155
+ end
156
+ end
157
+
158
+ def complex_object_head(node, depth)
159
+ head = INDENT*depth + node.rm_type_name
160
+ head += "[#{node.node_id}]" if node.node_id
161
+ head += " occurrences matches {#{interval_literal(node.occurrences)}}" if show_interval?(node.occurrences)
162
+ head
163
+ end
164
+
165
+ # A single attribute whose sole child is an inline-only node
166
+ # (C_PRIMITIVE_OBJECT, CONSTRAINT_REF, C_CODE_PHRASE,
167
+ # C_DV_ORDINAL) is flattened - its "matches {...}" IS the
168
+ # attribute's own matches clause, with no nested TYPE wrapper,
169
+ # matching real ADL 1.4 usage (e.g. "value matches {True}", not
170
+ # "value matches { BOOLEAN matches {True} }"). Anything else
171
+ # (C_COMPLEX_OBJECT, ARCHETYPE_SLOT, ARCHETYPE_INTERNAL_REF, or
172
+ # more than one alternative) renders as nested block(s).
173
+ def emit_c_attribute(attribute, depth)
174
+ head = attribute_head(attribute, depth)
175
+ children = attribute.children || []
176
+ if children.size == 1 && inline?(children.first)
177
+ head + " matches {#{inline_body(children.first)}}" + NL
178
+ else
179
+ body = children.map { |c| emit_c_object(c, depth + 1) }.join
180
+ head + ' matches {' + NL + body + INDENT*depth + '}' + NL
181
+ end
182
+ end
183
+
184
+ def attribute_head(attribute, depth)
185
+ head = INDENT*depth + attribute.rm_attribute_name
186
+ head += " existence matches {#{interval_literal(attribute.existence)}}" if show_interval?(attribute.existence)
187
+ head += " cardinality matches {#{cardinality_literal(attribute.cardinality)}}" if attribute.is_a?(CMultipleAttribute)
188
+ head
189
+ end
190
+
191
+ def inline?(node)
192
+ node.is_a?(CPrimitiveObject) || node.is_a?(ConstraintRef) ||
193
+ node.is_a?(OpenEHRProfile::DataTypes::Quantity::CDvOrdinal) ||
194
+ node.is_a?(OpenEHRProfile::DataTypes::Text::CCodePhrase)
195
+ end
196
+
197
+ def inline_body(node)
198
+ case node
199
+ when CPrimitiveObject
200
+ primitive_body(node.item)
201
+ when ConstraintRef
202
+ "[#{node.reference}]"
203
+ when OpenEHRProfile::DataTypes::Quantity::CDvOrdinal
204
+ ordinal_body(node)
205
+ when OpenEHRProfile::DataTypes::Text::CCodePhrase
206
+ code_phrase_body(node)
207
+ end
208
+ end
209
+
210
+ def emit_archetype_slot(node, depth)
211
+ head = INDENT*depth + 'allow_archetype ' + node.rm_type_name
212
+ head += "[#{node.node_id}]" if node.node_id
213
+ head += " occurrences matches {#{interval_literal(node.occurrences)}}" if show_interval?(node.occurrences)
214
+ lines = [head + ' matches {' + NL]
215
+ lines << assertion_block(node.includes, 'include', depth + 1)
216
+ lines << assertion_block(node.excludes, 'exclude', depth + 1)
217
+ lines << INDENT*depth + '}' + NL
218
+ lines.join
219
+ end
220
+
221
+ def assertion_block(assertions, keyword, depth)
222
+ return '' if assertions.nil?
223
+
224
+ lines = [INDENT*depth + keyword + NL]
225
+ assertions.each { |a| lines << INDENT*(depth+1) + a.string_expression + NL }
226
+ lines.join
227
+ end
228
+
229
+ def emit_internal_ref(node, depth)
230
+ INDENT*depth + "use_node #{node.rm_type_name} #{node.target_path}" + NL
231
+ end
232
+
233
+ def emit_constraint_ref(node, depth)
234
+ INDENT*depth + "[#{node.reference}]" + NL
235
+ end
236
+
237
+ def primitive_body(item)
238
+ return boolean_body(item) if item.is_a?(Primitive::CBoolean)
239
+
240
+ bounded_primitive_body(item)
241
+ end
242
+
243
+ def boolean_body(item)
244
+ values = []
245
+ values << 'True' if item.true_valid
246
+ values << 'False' if item.false_valid
247
+ with_assumed_value(values.join(', '), item)
248
+ end
249
+
250
+ def bounded_primitive_body(item)
251
+ body =
252
+ if item.respond_to?(:pattern) && item.pattern
253
+ item.pattern.to_s
254
+ elsif item.list
255
+ item.list.map { |v| primitive_literal(v) }.join(', ')
256
+ elsif item.range
257
+ "|#{range_literal(item.range)}|"
258
+ else
259
+ return '*'
260
+ end
261
+ with_assumed_value(body, item)
262
+ end
263
+
264
+ def with_assumed_value(body, item)
265
+ return body unless item.has_assumed_value?
266
+
267
+ "#{body}; #{primitive_literal(item.assumed_value)}"
268
+ end
269
+
270
+ def primitive_literal(value)
271
+ case value
272
+ when String then "\"#{value}\""
273
+ when true then 'True'
274
+ when false then 'False'
275
+ when Numeric then value.to_s
276
+ else
277
+ value.respond_to?(:value) ? value.value.to_s : value.to_s
278
+ end
279
+ end
280
+
281
+ def bound_literal(bound)
282
+ return nil if bound.nil?
283
+
284
+ bound.respond_to?(:value) ? bound.value.to_s : bound.to_s
285
+ end
286
+
287
+ def range_literal(range)
288
+ lo = bound_literal(range.lower)
289
+ up = bound_literal(range.upper)
290
+ if lo && up
291
+ lo == up ? lo : "#{lo}..#{up}"
292
+ elsif lo
293
+ "#{range.lower_included? ? '>=' : '>'}#{lo}"
294
+ elsif up
295
+ "#{range.upper_included? ? '<=' : '<'}#{up}"
296
+ else
297
+ '*'
298
+ end
299
+ end
300
+
301
+ def ordinal_body(node)
302
+ return 'C_DV_ORDINAL < >' if node.any_allowed?
303
+
304
+ body = node.list.map { |o| "#{o.value}|[#{o.symbol.code_string}]" }.join(', ')
305
+ with_assumed_value(body, node)
306
+ end
307
+
308
+ def code_phrase_body(node)
309
+ return '*' if node.any_allowed?
310
+
311
+ "[#{node.terminology_id.value}::#{node.code_list.join(', ')}]"
312
+ end
313
+
314
+ def show_interval?(interval)
315
+ return false if interval.nil?
316
+
317
+ !(interval.lower == 1 && interval.upper == 1)
318
+ end
319
+
320
+ def interval_literal(interval)
321
+ lo = interval.lower_unbounded? ? '0' : interval.lower.to_s
322
+ up = interval.upper_unbounded? ? '*' : interval.upper.to_s
323
+ "#{lo}..#{up}"
324
+ end
325
+
326
+ def cardinality_literal(cardinality)
327
+ return '0..*; unordered' if cardinality.nil?
328
+
329
+ flags = [cardinality.is_ordered? ? 'ordered' : 'unordered']
330
+ flags << 'unique' if cardinality.is_unique?
331
+ "#{interval_literal(cardinality.interval)}; #{flags.join('; ')}"
332
+ end
333
+ end
334
+ end
335
+ end
@@ -0,0 +1,20 @@
1
+ module OpenEHR
2
+ module Serializer
3
+ NL = "\r\n"
4
+ INDENT = ' '
5
+
6
+ class BaseSerializer
7
+ def initialize(archetype)
8
+ @archetype = archetype
9
+ end
10
+
11
+ def serialize
12
+ return self.merge
13
+ end
14
+
15
+ private
16
+ def merge
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,49 @@
1
+ require 'json'
2
+ require_relative 'base'
3
+
4
+ module OpenEHR
5
+ module Serializer
6
+ class OPTSerializer < BaseSerializer
7
+ def initialize(opt, format:)
8
+ @opt = OpenEHR::Parser::OPTParser.new(opt).parse
9
+ @format = format
10
+ end
11
+
12
+ def name
13
+ @opt.definition.archetype_id.concept_name
14
+ end
15
+
16
+ # Template-level metadata, as JSON. An operational template
17
+ # carries constraints, not instance data - it has no composer or
18
+ # territory to report - so this is the concept/ids/language the
19
+ # OPT actually captures, rather than a simulated RM COMPOSITION.
20
+ def header
21
+ JSON.generate(
22
+ 'concept' => @opt.concept,
23
+ 'archetype_id' => @opt.definition.archetype_id.value,
24
+ 'template_id' => @opt.template_id.value,
25
+ 'language' => @opt.original_language.code_string
26
+ )
27
+ end
28
+
29
+ # The root archetype's context/content constraint subtrees (what
30
+ # the template actually constrains for those attributes), as
31
+ # JSON via JSONSerializer - the same generic walker
32
+ # RMJSONSerializer uses for RM instances.
33
+ def context
34
+ attribute_json('context')
35
+ end
36
+
37
+ def content
38
+ attribute_json('content')
39
+ end
40
+
41
+ private
42
+
43
+ def attribute_json(rm_attribute_name)
44
+ attribute = @opt.definition.attributes.find { |a| a.rm_attribute_name == rm_attribute_name }
45
+ JSONSerializer.new(attribute).serialize
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,62 @@
1
+ require 'json'
2
+ require 'set'
3
+
4
+ module OpenEHR
5
+ module Serializer
6
+ # Canonical JSON for an RM instance graph: a generic reflection
7
+ # walker (not a hand-written serializer per RM class), tagging
8
+ # each object with a "_type" discriminator (via
9
+ # OpenEHR::RM.type_name_of) and recursing into every non-nil
10
+ # instance variable except @parent (PATHABLE's back-reference,
11
+ # which would otherwise make every subtree cyclic). A defensive
12
+ # visited-object guard covers any other unexpected back-reference.
13
+ class RMJSONSerializer
14
+ EXCLUDED_IVARS = [:@parent].freeze
15
+
16
+ def initialize(rm_instance)
17
+ @rm_instance = rm_instance
18
+ end
19
+
20
+ def serialize
21
+ JSON.generate(to_value(@rm_instance, Set.new.compare_by_identity))
22
+ end
23
+
24
+ private
25
+
26
+ def to_value(value, seen)
27
+ case value
28
+ when nil, true, false, Numeric, String
29
+ value
30
+ when Array
31
+ value.map { |v| to_value(v, seen) }
32
+ when Hash
33
+ value.each_with_object({}) { |(k, v), h| h[k.to_s] = to_value(v, seen) }
34
+ else
35
+ object_value(value, seen)
36
+ end
37
+ end
38
+
39
+ def object_value(value, seen)
40
+ return nil if seen.include?(value)
41
+
42
+ seen << value
43
+ hash = {'_type' => OpenEHR::RM.type_name_of(value)}
44
+ (value.instance_variables - EXCLUDED_IVARS).each do |ivar|
45
+ field = value.instance_variable_get(ivar)
46
+ next if field.nil?
47
+
48
+ hash[ivar.to_s.delete_prefix('@')] = to_value(field, seen)
49
+ end
50
+ hash
51
+ end
52
+ end
53
+
54
+ # RMJSONSerializer's reflection walker has no RM-specific logic
55
+ # (it derives "_type" from each object's own class and excludes
56
+ # only the PATHABLE/ArchetypeConstraint @parent back-reference),
57
+ # so the same class already serializes AOM constraint trees
58
+ # (CComplexObject, CAttribute, ...) correctly. Exposed under this
59
+ # name for that use.
60
+ JSONSerializer = RMJSONSerializer
61
+ end
62
+ end