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,105 @@
1
+ module OpenEHR
2
+ module AQL
3
+ module Model
4
+ # whereClause : WHERE whereExpr ;
5
+ class WhereClause
6
+ attr_reader :expression
7
+
8
+ def initialize(expression:)
9
+ @expression = expression
10
+ freeze
11
+ end
12
+ end
13
+
14
+ # whereExpr : ... | whereExpr AND whereExpr | ... ;
15
+ class AndExpr
16
+ attr_reader :left, :right
17
+
18
+ def initialize(left:, right:)
19
+ @left = left
20
+ @right = right
21
+ freeze
22
+ end
23
+ end
24
+
25
+ # whereExpr : ... | whereExpr OR whereExpr | ... ;
26
+ class OrExpr
27
+ attr_reader :left, :right
28
+
29
+ def initialize(left:, right:)
30
+ @left = left
31
+ @right = right
32
+ freeze
33
+ end
34
+ end
35
+
36
+ # whereExpr : ... | NOT whereExpr | ... ;
37
+ class NotExpr
38
+ attr_reader :operand
39
+
40
+ def initialize(operand:)
41
+ @operand = operand
42
+ freeze
43
+ end
44
+ end
45
+
46
+ # identifiedExpr : identifiedPath COMPARISON_OPERATOR terminal | functionCall COMPARISON_OPERATOR terminal ;
47
+ # The functionCall-on-the-left alternative is added by M8.
48
+ class Comparison
49
+ attr_reader :left, :operator, :right
50
+
51
+ def initialize(left:, operator:, right:)
52
+ @left = left
53
+ @operator = operator
54
+ @right = right
55
+ freeze
56
+ end
57
+ end
58
+
59
+ # identifiedExpr : EXISTS identifiedPath | ... ;
60
+ class ExistsExpr
61
+ attr_reader :path
62
+
63
+ def initialize(path:)
64
+ @path = path
65
+ freeze
66
+ end
67
+ end
68
+
69
+ # identifiedExpr : identifiedPath LIKE likeOperand | ... ;
70
+ class LikeExpr
71
+ attr_reader :path, :operand
72
+
73
+ def initialize(path:, operand:)
74
+ @path = path
75
+ @operand = operand
76
+ freeze
77
+ end
78
+ end
79
+
80
+ # identifiedExpr : identifiedPath MATCHES matchesOperand | ... ;
81
+ class MatchesExpr
82
+ attr_reader :path, :operand
83
+
84
+ def initialize(path:, operand:)
85
+ @path = path
86
+ @operand = operand
87
+ freeze
88
+ end
89
+ end
90
+
91
+ # matchesOperand's value-list alternative:
92
+ # SYM_LEFT_CURLY valueListItem (SYM_COMMA valueListItem)* SYM_RIGHT_CURLY
93
+ # The terminologyFunction-item and bare-URI alternatives need a URI
94
+ # lexer token that doesn't exist yet, and are deferred.
95
+ class MatchesValueList
96
+ attr_reader :items
97
+
98
+ def initialize(items:)
99
+ @items = items.freeze
100
+ freeze
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,14 @@
1
+ # OpenEHR::AQL::Model holds the immutable object model that the Parser
2
+ # builds and the execution Engine walks: one class per relevant
3
+ # AqlParser.g4 rule (see lib/openehr/aql/parser.rb for the mapping).
4
+ require_relative 'model/query'
5
+ require_relative 'model/select_clause'
6
+ require_relative 'model/from_clause'
7
+ require_relative 'model/identified_path'
8
+ require_relative 'model/literal'
9
+ require_relative 'model/object_path'
10
+ require_relative 'model/predicate'
11
+ require_relative 'model/containment'
12
+ require_relative 'model/where_clause'
13
+ require_relative 'model/order_by_and_limit'
14
+ require_relative 'model/function_call'
@@ -0,0 +1,484 @@
1
+ require_relative 'lexer'
2
+ require_relative 'model'
3
+
4
+ module OpenEHR
5
+ module AQL
6
+ # Recursive-descent parser over the Lexer's token stream. Method names
7
+ # mirror the official AqlParser.g4 rule names (see the rule cited in
8
+ # each method's comment) so the implementation stays traceable against
9
+ # the reference grammar. Each project-plan milestone implements a
10
+ # subset of these rules; a construct a milestone hasn't reached yet is
11
+ # simply never consumed, so parsing it falls through to the nearest
12
+ # "expected X, got Y" ParseError instead of being silently misparsed.
13
+ class Parser
14
+ def initialize(tokens)
15
+ @tokens = tokens
16
+ @pos = 0
17
+ end
18
+
19
+ # selectQuery : selectClause fromClause whereClause? orderByClause? limitClause? SYM_DOUBLE_DASH? EOF ;
20
+ # The trailing SYM_DOUBLE_DASH needs no explicit handling here: the
21
+ # Lexer's COMMENT rule already treats a "--" immediately followed by
22
+ # EOF (or a space, or a newline) as an empty/line comment and skips
23
+ # it, so a trailing "--" is silently absorbed before the parser ever
24
+ # sees a token for it.
25
+ def parse_select_query
26
+ select_clause = parse_select_clause
27
+ from_clause = parse_from_clause
28
+ where_clause = check(:where) ? parse_where_clause : nil
29
+ order_by_clause = check(:order) ? parse_order_by_clause : nil
30
+ limit_clause = check(:limit) ? parse_limit_clause : nil
31
+ expect(:eof)
32
+ Model::Query.new(select_clause: select_clause, from_clause: from_clause, where_clause: where_clause,
33
+ order_by_clause: order_by_clause, limit_clause: limit_clause)
34
+ end
35
+
36
+ PRIMITIVE_TOKEN_TYPES = %i[string integer real sci_integer sci_real boolean null].freeze
37
+ DESCENDING_DIRECTIONS = %i[desc descending].freeze
38
+ AGGREGATE_FUNCTION_TYPES = %i[count min max sum avg].freeze
39
+ FUNCTION_NAME_TYPES = %i[length position substring concat concat_ws abs mod ceil floor round
40
+ now current_date current_time current_date_time current_timezone].freeze
41
+
42
+ private
43
+
44
+ # selectClause : SELECT DISTINCT? top? selectExpr (SYM_COMMA selectExpr)* ;
45
+ def parse_select_clause
46
+ expect(:select)
47
+ distinct = match(:distinct)
48
+ top = check(:top) ? parse_top : nil
49
+ columns = [parse_select_expr]
50
+ columns << parse_select_expr while match(:comma)
51
+ Model::SelectClause.new(columns: columns, distinct: distinct, top: top)
52
+ end
53
+
54
+ # top : TOP INTEGER direction=(FORWARD|BACKWARD)? ; (deprecated)
55
+ def parse_top
56
+ expect(:top)
57
+ count = expect(:integer).value
58
+ direction = if match(:forward)
59
+ :forward
60
+ elsif match(:backward)
61
+ :backward
62
+ end
63
+ Model::Top.new(count: count, direction: direction)
64
+ end
65
+
66
+ # selectExpr : columnExpr (AS aliasName=IDENTIFIER)? ;
67
+ def parse_select_expr
68
+ expression = parse_column_expr
69
+ alias_name = match(:as) ? expect(:identifier).value : nil
70
+ Model::SelectColumn.new(expression: expression, alias_name: alias_name)
71
+ end
72
+
73
+ # columnExpr : identifiedPath | primitive | aggregateFunctionCall | functionCall ;
74
+ def parse_column_expr
75
+ return parse_aggregate_function_call if aggregate_function_ahead?
76
+ return parse_function_call if function_call_ahead?
77
+ return parse_identified_path if check(:identifier)
78
+ return parse_primitive if primitive_ahead?
79
+
80
+ raise error("expected a path, a literal value or a function call, got #{describe(peek)}")
81
+ end
82
+
83
+ # fromClause : FROM fromExpr ; fromExpr : containsExpr ;
84
+ def parse_from_clause
85
+ expect(:from)
86
+ Model::FromClause.new(containment: parse_contains_or_expr)
87
+ end
88
+
89
+ # containsExpr : ... | containsExpr OR containsExpr | ... ;
90
+ # AND binds tighter than OR (same precedence-climbing rewrite of the
91
+ # reference grammar's left recursion as whereExpr, M5).
92
+ def parse_contains_or_expr
93
+ left = parse_contains_and_expr
94
+ left = Model::ContainmentOr.new(left: left, right: parse_contains_and_expr) while match(:or)
95
+ left
96
+ end
97
+
98
+ # containsExpr : ... | containsExpr AND containsExpr | ... ;
99
+ def parse_contains_and_expr
100
+ left = parse_contains_primary
101
+ left = Model::ContainmentAnd.new(left: left, right: parse_contains_primary) while match(:and)
102
+ left
103
+ end
104
+
105
+ # containsExpr : ... | SYM_LEFT_PAREN containsExpr SYM_RIGHT_PAREN ;
106
+ def parse_contains_primary
107
+ return parse_containment_chain unless match(:left_paren)
108
+
109
+ expr = parse_contains_or_expr
110
+ expect(:right_paren)
111
+ expr
112
+ end
113
+
114
+ # containsExpr : classExprOperand (NOT? CONTAINS containsExpr)? ;
115
+ # The right-recursive "A CONTAINS B CONTAINS C ..." chain, with an
116
+ # optional NOT flag on each containment edge.
117
+ def parse_containment_chain
118
+ operand = parse_class_expr_operand
119
+ negated = match(:not)
120
+ if negated
121
+ expect(:contains)
122
+ elsif !match(:contains)
123
+ return operand
124
+ end
125
+ Model::Containment.new(parent: operand, child: parse_contains_or_expr, negated: negated)
126
+ end
127
+
128
+ # classExprOperand : IDENTIFIER variable=IDENTIFIER? pathPredicate? #classExpression | ... ;
129
+ # (archetypePredicate/nodePredicate alternatives inside pathPredicate are added by M3)
130
+ def parse_class_expr_operand
131
+ class_name = expect(:identifier).value
132
+ variable = check(:identifier) ? advance.value : nil
133
+ predicate = check(:left_bracket) ? parse_path_predicate : nil
134
+ Model::ClassExpression.new(class_name: class_name, variable: variable, predicate: predicate)
135
+ end
136
+
137
+ # pathPredicate : SYM_LEFT_BRACKET (standardPredicate | archetypePredicate | nodePredicate) SYM_RIGHT_BRACKET ;
138
+ # standardPredicate's shape (objectPath COMPARISON_OPERATOR
139
+ # pathPredicateOperand) is literally nodePredicate's 4th
140
+ # alternative, and archetypePredicate/nodePredicate's bare-code
141
+ # forms both take the same optional SYM_COMMA suffix - so rather
142
+ # than dispatch to three separate rules, the bracket content is
143
+ # parsed as one predicate-expression grammar (bare code/archetype-id/
144
+ # parameter, or a path comparison, combined with AND/OR - same
145
+ # precedence-climbing shape as whereExpr/containsExpr).
146
+ def parse_path_predicate
147
+ expect(:left_bracket)
148
+ predicate = parse_predicate_or
149
+ expect(:right_bracket)
150
+ predicate
151
+ end
152
+
153
+ # nodePredicate : ... | nodePredicate OR nodePredicate ;
154
+ def parse_predicate_or
155
+ left = parse_predicate_and
156
+ left = Model::PredicateOr.new(left: left, right: parse_predicate_and) while match(:or)
157
+ left
158
+ end
159
+
160
+ # nodePredicate : ... | nodePredicate AND nodePredicate ;
161
+ def parse_predicate_and
162
+ left = parse_predicate_primary
163
+ left = Model::PredicateAnd.new(left: left, right: parse_predicate_primary) while match(:and)
164
+ left
165
+ end
166
+
167
+ # archetypePredicate : ARCHETYPE_HRID (SYM_COMMA ...)? | PARAMETER ;
168
+ # nodePredicate : (ID_CODE | AT_CODE) (SYM_COMMA ...)? | ARCHETYPE_HRID (SYM_COMMA ...)? | PARAMETER
169
+ # | objectPath COMPARISON_OPERATOR pathPredicateOperand | objectPath MATCHES CONTAINED_REGEX ;
170
+ # objectPath MATCHES CONTAINED_REGEX needs a lexer token this gem
171
+ # doesn't implement yet (deferred, same as elsewhere).
172
+ def parse_predicate_primary
173
+ return parse_archetype_predicate if check(:archetype_hrid)
174
+ return parse_code_predicate if check(:at_code) || check(:id_code)
175
+ return Model::Parameter.new(name: advance.value) if check(:parameter)
176
+
177
+ parse_standard_predicate
178
+ end
179
+
180
+ def parse_archetype_predicate
181
+ archetype_id = expect(:archetype_hrid).value
182
+ value = match(:comma) ? parse_node_predicate_value : nil
183
+ Model::ArchetypePredicate.new(archetype_id: archetype_id, value: value)
184
+ end
185
+
186
+ def parse_code_predicate
187
+ code = advance.value
188
+ value = match(:comma) ? parse_node_predicate_value : nil
189
+ Model::NodePredicate.new(code: code, value: value)
190
+ end
191
+
192
+ # The SYM_COMMA suffix's value set: STRING | PARAMETER | TERM_CODE | AT_CODE | ID_CODE.
193
+ # TERM_CODE needs a lexer token this gem doesn't implement yet
194
+ # (deferred, same as elsewhere) and is not supported here.
195
+ def parse_node_predicate_value
196
+ return Model::Parameter.new(name: advance.value) if check(:parameter)
197
+ return advance.value if check(:string) || check(:at_code) || check(:id_code)
198
+
199
+ raise error("expected a string, parameter, at-code or id-code after ',', got #{describe(peek)}")
200
+ end
201
+
202
+ # standardPredicate : objectPath COMPARISON_OPERATOR pathPredicateOperand ;
203
+ def parse_standard_predicate
204
+ path = parse_object_path
205
+ operator = expect(:comparison_operator).value
206
+ Model::StandardPredicate.new(path: path, operator: operator, operand: parse_path_predicate_operand)
207
+ end
208
+
209
+ # pathPredicateOperand : primitive | objectPath | PARAMETER | ID_CODE | AT_CODE ;
210
+ # The bare ID_CODE/AT_CODE alternatives (distinct from an objectPath
211
+ # that happens to start with one - objectPath's pathPart is always
212
+ # IDENTIFIER, never a code) aren't needed by any current example and
213
+ # are deferred.
214
+ def parse_path_predicate_operand
215
+ return Model::Parameter.new(name: advance.value) if check(:parameter)
216
+ return parse_primitive if primitive_ahead?
217
+ return parse_object_path if check(:identifier)
218
+
219
+ raise error("expected a parameter, a literal value or a path, got #{describe(peek)}")
220
+ end
221
+
222
+ # objectPath : pathPart (SYM_SLASH pathPart)* ;
223
+ def parse_object_path
224
+ parts = [parse_path_part]
225
+ parts << parse_path_part while match(:slash)
226
+ Model::ObjectPath.new(segments: parts)
227
+ end
228
+
229
+ # pathPart : IDENTIFIER pathPredicate? ;
230
+ def parse_path_part
231
+ attribute = expect(:identifier).value
232
+ predicate = check(:left_bracket) ? parse_path_predicate : nil
233
+ Model::PathPart.new(attribute: attribute, predicate: predicate)
234
+ end
235
+
236
+ # identifiedPath : IDENTIFIER pathPredicate? (SYM_SLASH objectPath)? ;
237
+ def parse_identified_path
238
+ variable = expect(:identifier).value
239
+ predicate = check(:left_bracket) ? parse_path_predicate : nil
240
+ path = match(:slash) ? parse_object_path : nil
241
+ Model::IdentifiedPath.new(variable: variable, predicate: predicate, path: path)
242
+ end
243
+
244
+ # whereClause : WHERE whereExpr ;
245
+ def parse_where_clause
246
+ expect(:where)
247
+ Model::WhereClause.new(expression: parse_or_expr)
248
+ end
249
+
250
+ # whereExpr's left-recursive AND/OR/NOT alternatives, rewritten as
251
+ # standard precedence climbing (NOT tightest, then AND, then OR -
252
+ # the usual boolean-operator precedence, matching how the official
253
+ # examples read without needing extra parens).
254
+ def parse_or_expr
255
+ left = parse_and_expr
256
+ left = Model::OrExpr.new(left: left, right: parse_and_expr) while match(:or)
257
+ left
258
+ end
259
+
260
+ def parse_and_expr
261
+ left = parse_not_expr
262
+ left = Model::AndExpr.new(left: left, right: parse_not_expr) while match(:and)
263
+ left
264
+ end
265
+
266
+ # whereExpr : ... | NOT whereExpr | ... ;
267
+ def parse_not_expr
268
+ return Model::NotExpr.new(operand: parse_not_expr) if match(:not)
269
+
270
+ parse_where_primary
271
+ end
272
+
273
+ # whereExpr : ... | SYM_LEFT_PAREN whereExpr SYM_RIGHT_PAREN ;
274
+ # (identifiedExpr's own, narrower "( identifiedExpr )" alternative is
275
+ # already covered by this, since identifiedExpr is one kind of
276
+ # whereExpr.)
277
+ def parse_where_primary
278
+ return parse_identified_expr unless match(:left_paren)
279
+
280
+ expr = parse_or_expr
281
+ expect(:right_paren)
282
+ expr
283
+ end
284
+
285
+ # identifiedExpr : EXISTS identifiedPath
286
+ # | identifiedPath COMPARISON_OPERATOR terminal
287
+ # | identifiedPath LIKE likeOperand
288
+ # | identifiedPath MATCHES matchesOperand ;
289
+ # The functionCall-on-the-left comparison alternative is added by M8.
290
+ def parse_identified_expr
291
+ return Model::ExistsExpr.new(path: parse_identified_path) if match(:exists)
292
+
293
+ path = parse_identified_path
294
+ if check(:comparison_operator)
295
+ Model::Comparison.new(left: path, operator: advance.value, right: parse_terminal)
296
+ elsif match(:like)
297
+ Model::LikeExpr.new(path: path, operand: parse_like_operand)
298
+ elsif match(:matches)
299
+ Model::MatchesExpr.new(path: path, operand: parse_matches_operand)
300
+ else
301
+ raise error("expected a comparison operator, LIKE or MATCHES, got #{describe(peek)}")
302
+ end
303
+ end
304
+
305
+ # terminal : primitive | PARAMETER | identifiedPath | functionCall ;
306
+ def parse_terminal
307
+ return Model::Parameter.new(name: advance.value) if check(:parameter)
308
+ return parse_function_call if function_call_ahead?
309
+ return parse_primitive if primitive_ahead?
310
+ return parse_identified_path if check(:identifier)
311
+
312
+ raise error("expected a value, parameter, path or function call, got #{describe(peek)}")
313
+ end
314
+
315
+ # likeOperand : STRING | PARAMETER ;
316
+ def parse_like_operand
317
+ return Model::Parameter.new(name: advance.value) if check(:parameter)
318
+
319
+ Model::Literal.new(value: expect(:string).value)
320
+ end
321
+
322
+ # matchesOperand : SYM_LEFT_CURLY valueListItem (SYM_COMMA valueListItem)* SYM_RIGHT_CURLY
323
+ # | terminologyFunction
324
+ # | SYM_LEFT_CURLY URI SYM_RIGHT_CURLY ;
325
+ def parse_matches_operand
326
+ return parse_terminology_function if check(:terminology)
327
+
328
+ expect(:left_curly)
329
+ operand = if check(:uri)
330
+ Model::UriRef.new(uri: advance.value)
331
+ else
332
+ items = [parse_value_list_item]
333
+ items << parse_value_list_item while match(:comma)
334
+ Model::MatchesValueList.new(items: items)
335
+ end
336
+ expect(:right_curly)
337
+ operand
338
+ end
339
+
340
+ # valueListItem : primitive | PARAMETER | terminologyFunction ;
341
+ def parse_value_list_item
342
+ return Model::Parameter.new(name: advance.value) if check(:parameter)
343
+ return parse_terminology_function if check(:terminology)
344
+ return parse_primitive if primitive_ahead?
345
+
346
+ raise error("expected a literal value, a parameter or TERMINOLOGY(...) in a MATCHES value list, " \
347
+ "got #{describe(peek)}")
348
+ end
349
+
350
+ # terminologyFunction : TERMINOLOGY SYM_LEFT_PAREN STRING SYM_COMMA STRING SYM_COMMA STRING SYM_RIGHT_PAREN ;
351
+ def parse_terminology_function
352
+ expect(:terminology)
353
+ expect(:left_paren)
354
+ args = [expect(:string).value]
355
+ 2.times do
356
+ expect(:comma)
357
+ args << expect(:string).value
358
+ end
359
+ expect(:right_paren)
360
+ Model::TerminologyFunctionCall.new(args: args)
361
+ end
362
+
363
+ # aggregateFunctionCall : name=COUNT SYM_LEFT_PAREN (DISTINCT? identifiedPath | SYM_ASTERISK) SYM_RIGHT_PAREN
364
+ # | name=(MIN | MAX | SUM | AVG) SYM_LEFT_PAREN identifiedPath SYM_RIGHT_PAREN ;
365
+ def parse_aggregate_function_call
366
+ name = advance.type
367
+ expect(:left_paren)
368
+ if name == :count && check(:asterisk)
369
+ advance
370
+ path = nil
371
+ distinct = false
372
+ else
373
+ distinct = name == :count && match(:distinct)
374
+ path = parse_identified_path
375
+ end
376
+ expect(:right_paren)
377
+ Model::AggregateFunctionCall.new(name: name, path: path, distinct: distinct)
378
+ end
379
+
380
+ def aggregate_function_ahead?
381
+ AGGREGATE_FUNCTION_TYPES.include?(peek.type) && peek(1).type == :left_paren
382
+ end
383
+
384
+ # functionCall : terminologyFunction
385
+ # | name=(STRING_FUNCTION_ID | NUMERIC_FUNCTION_ID | DATE_TIME_FUNCTION_ID | IDENTIFIER)
386
+ # SYM_LEFT_PAREN (terminal (SYM_COMMA terminal)*)? SYM_RIGHT_PAREN ;
387
+ # The functionCall-on-the-left-of-a-comparison identifiedExpr
388
+ # alternative isn't needed by any example and is deferred.
389
+ def parse_function_call
390
+ return parse_terminology_function if check(:terminology)
391
+
392
+ name = advance.value
393
+ expect(:left_paren)
394
+ args = []
395
+ unless check(:right_paren)
396
+ args << parse_terminal
397
+ args << parse_terminal while match(:comma)
398
+ end
399
+ expect(:right_paren)
400
+ Model::FunctionCall.new(name: name, arguments: args)
401
+ end
402
+
403
+ def function_call_ahead?
404
+ return true if check(:terminology)
405
+
406
+ (FUNCTION_NAME_TYPES.include?(peek.type) || check(:identifier)) && peek(1).type == :left_paren
407
+ end
408
+
409
+ # orderByClause : ORDER BY orderByExpr (SYM_COMMA orderByExpr)* ;
410
+ def parse_order_by_clause
411
+ expect(:order)
412
+ expect(:by)
413
+ items = [parse_order_by_expr]
414
+ items << parse_order_by_expr while match(:comma)
415
+ Model::OrderByClause.new(items: items)
416
+ end
417
+
418
+ # orderByExpr : identifiedPath order=(DESCENDING|DESC|ASCENDING|ASC)? ;
419
+ def parse_order_by_expr
420
+ path = parse_identified_path
421
+ direction = :asc
422
+ if check(:desc) || check(:descending) || check(:asc) || check(:ascending)
423
+ direction = DESCENDING_DIRECTIONS.include?(advance.type) ? :desc : :asc
424
+ end
425
+ Model::OrderByItem.new(path: path, direction: direction)
426
+ end
427
+
428
+ # limitClause : LIMIT limit=INTEGER (OFFSET offset=INTEGER)? ;
429
+ def parse_limit_clause
430
+ expect(:limit)
431
+ limit = expect(:integer).value
432
+ offset = match(:offset) ? expect(:integer).value : 0
433
+ Model::LimitClause.new(limit: limit, offset: offset)
434
+ end
435
+
436
+ def primitive_ahead?
437
+ PRIMITIVE_TOKEN_TYPES.include?(peek.type)
438
+ end
439
+
440
+ # primitive : STRING | numericPrimitive | DATE | TIME | DATETIME | BOOLEAN | NULL ;
441
+ def parse_primitive
442
+ token = advance
443
+ Model::Literal.new(value: token.type == :null ? nil : token.value)
444
+ end
445
+
446
+ # --- token stream helpers ---
447
+
448
+ def peek(offset = 0)
449
+ @tokens[@pos + offset] || @tokens.last
450
+ end
451
+
452
+ def check(type)
453
+ peek.type == type
454
+ end
455
+
456
+ def advance
457
+ token = peek
458
+ @pos += 1 unless token.type == :eof
459
+ token
460
+ end
461
+
462
+ def match(type)
463
+ return false unless check(type)
464
+
465
+ advance
466
+ true
467
+ end
468
+
469
+ def expect(type)
470
+ return advance if check(type)
471
+
472
+ raise error("expected #{type.to_s.upcase}, got #{describe(peek)}")
473
+ end
474
+
475
+ def describe(token)
476
+ token.type == :eof ? 'end of input' : "#{token.type} #{token.value.inspect}"
477
+ end
478
+
479
+ def error(message)
480
+ ParseError.new(message, line: peek.line, column: peek.column)
481
+ end
482
+ end
483
+ end
484
+ end
@@ -0,0 +1,54 @@
1
+ require 'json'
2
+ require_relative '../serializer/rm_json_serializer'
3
+
4
+ module OpenEHR
5
+ module AQL
6
+ # The output of Query#execute: column names plus rows (each row is an
7
+ # Array positionally aligned with `columns`).
8
+ class ResultSet
9
+ include Enumerable
10
+
11
+ attr_reader :columns, :rows
12
+
13
+ def initialize(columns:, rows:)
14
+ @columns = columns.freeze
15
+ @rows = rows.freeze
16
+ freeze
17
+ end
18
+
19
+ def each
20
+ return enum_for(:each) unless block_given?
21
+
22
+ rows.each { |row| yield row }
23
+ end
24
+
25
+ def size
26
+ rows.size
27
+ end
28
+
29
+ # The openEHR REST Query API's result-set shape (columns/rows),
30
+ # scoped to what's useful here rather than every optional metadata
31
+ # field ("meta", "name", "q", ...) the full spec allows - add them
32
+ # if/when a caller needs them. Each RM object row value is
33
+ # expanded via RMJSONSerializer; JSON primitives (String/Numeric/
34
+ # true/false/nil) pass through unchanged.
35
+ def to_json(*args)
36
+ {
37
+ 'columns' => columns.map { |name| { 'name' => name } },
38
+ 'rows' => rows.map { |row| row.map { |value| json_value(value) } }
39
+ }.to_json(*args)
40
+ end
41
+
42
+ private
43
+
44
+ def json_value(value)
45
+ case value
46
+ when nil, true, false, Numeric, String
47
+ value
48
+ else
49
+ JSON.parse(Serializer::RMJSONSerializer.new(value).serialize)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,28 @@
1
+ require_relative 'aql/errors'
2
+ require_relative 'aql/lexer'
3
+ require_relative 'aql/model'
4
+ require_relative 'aql/parser'
5
+ require_relative 'aql/engine/dataset'
6
+ require_relative 'aql/engine'
7
+
8
+ # OpenEHR::AQL is the Archetype Query Language subsystem: a hand-written
9
+ # lexer/parser producing a Query object model (see the project plan's
10
+ # "AQLサブシステム設計" section), plus an in-memory execution engine that
11
+ # evaluates queries against Dataset (framework/ORM-agnostic; see
12
+ # lib/openehr/aql/engine/dataset.rb).
13
+ #
14
+ # Public API:
15
+ # OpenEHR::AQL.parse(aql_string) -> Model::Query
16
+ # query.execute(dataset, params: {}) -> ResultSet
17
+ # OpenEHR::AQL.execute(aql_string, dataset, params: {}) -> ResultSet
18
+ module OpenEHR
19
+ module AQL
20
+ def self.parse(source)
21
+ Parser.new(Lexer.new(source).tokenize).parse_select_query
22
+ end
23
+
24
+ def self.execute(source, dataset, params: {})
25
+ parse(source).execute(dataset, params: params)
26
+ end
27
+ end
28
+ end