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,295 @@
1
+ require_relative 'errors'
2
+
3
+ # OpenEHR::AQL::Lexer tokenizes AQL source text, following the official
4
+ # ANTLR4 lexer grammar (AqlLexer.g4,
5
+ # https://specifications.openehr.org/releases/QUERY/latest/AQL.html) as
6
+ # the reference spec. It is a hand-written scanner (no parser-generator
7
+ # dependency is added) so that maximal-munch tie-breaks between
8
+ # overlapping token shapes - e.g. "openEHR-EHR-OBSERVATION.blood_pressure.v1"
9
+ # (ARCHETYPE_HRID) vs "openEHR" (IDENTIFIER), or "id5" (ID_CODE) vs "id5x"
10
+ # (IDENTIFIER) - can be resolved explicitly per token category, matching
11
+ # ANTLR's longest-match-wins / first-rule-wins-on-tie semantics.
12
+ #
13
+ # Scope (M0 milestone): keywords (case-insensitive), identifiers,
14
+ # ARCHETYPE_HRID, AT_CODE/ID_CODE, STRING, INTEGER/REAL/SCI_*, BOOLEAN,
15
+ # $PARAMETER, comparison operators, structural symbols, and '--' comments.
16
+ # DATE/TIME/DATETIME literals, TERM_CODE, URI, CONTAINED_REGEX and a
17
+ # Unicode BOM are deferred to the milestone that first needs them - none
18
+ # of the official AQL examples this gem targets require them yet.
19
+ module OpenEHR
20
+ module AQL
21
+ class Token
22
+ attr_reader :type, :value, :line, :column
23
+
24
+ def initialize(type, value, line:, column:)
25
+ @type = type
26
+ @value = value
27
+ @line = line
28
+ @column = column
29
+ freeze
30
+ end
31
+
32
+ def ==(other)
33
+ other.is_a?(Token) && type == other.type && value == other.value
34
+ end
35
+ alias eql? ==
36
+
37
+ def hash
38
+ [type, value].hash
39
+ end
40
+
41
+ def to_s
42
+ "#{type}(#{value.inspect})@#{line}:#{column}"
43
+ end
44
+ end
45
+
46
+ class Lexer
47
+ KEYWORDS = {
48
+ 'select' => :select, 'as' => :as, 'from' => :from, 'where' => :where,
49
+ 'order' => :order, 'by' => :by, 'desc' => :desc, 'descending' => :descending,
50
+ 'asc' => :asc, 'ascending' => :ascending, 'limit' => :limit, 'offset' => :offset,
51
+ 'distinct' => :distinct, 'version' => :version, 'latest_version' => :latest_version,
52
+ 'all_versions' => :all_versions, 'null' => :null,
53
+ 'top' => :top, 'forward' => :forward, 'backward' => :backward,
54
+ 'contains' => :contains, 'and' => :and, 'or' => :or, 'not' => :not, 'exists' => :exists,
55
+ 'like' => :like, 'matches' => :matches,
56
+ 'count' => :count, 'min' => :min, 'max' => :max, 'sum' => :sum, 'avg' => :avg,
57
+ 'terminology' => :terminology,
58
+ 'length' => :length, 'position' => :position, 'substring' => :substring,
59
+ 'concat' => :concat, 'concat_ws' => :concat_ws,
60
+ 'abs' => :abs, 'mod' => :mod, 'ceil' => :ceil, 'floor' => :floor, 'round' => :round,
61
+ 'now' => :now, 'current_date' => :current_date, 'current_time' => :current_time,
62
+ 'current_date_time' => :current_date_time, 'current_timezone' => :current_timezone
63
+ }.freeze
64
+
65
+ ARCHETYPE_HRID_RE = /\A[A-Za-z][A-Za-z0-9_]*-[A-Za-z][A-Za-z0-9_]*-[A-Za-z][A-Za-z0-9_]*
66
+ \.[A-Za-z][A-Za-z0-9_-]*
67
+ \.v\d+(?:\.\d+)*(?:-(?:rc|alpha)(?:\.\d+)?)?/x
68
+ AT_ID_CODE_RE = /\A(?:at|id)\d+(?:\.\d+)*/
69
+ IDENTIFIER_RE = /\A[A-Za-z][A-Za-z0-9_]*/
70
+ URI_CHAR = %r{[A-Za-z0-9\-._~%!$&'()*+,;=:@/]}.source
71
+ URI_RE = %r{\A[A-Za-z][A-Za-z0-9+.-]*:(?://)?#{URI_CHAR}*(?:\?#{URI_CHAR}*)?(?:\##{URI_CHAR}*)?}o
72
+ NUMBER_RE = /\A(?:\d*\.\d+|\d+)(?:[eE][+-]?\d+)?/
73
+
74
+ ESCAPES = {
75
+ '\\' => "\\", "'" => "'", '"' => '"', '?' => '?',
76
+ 'a' => "\a", 'b' => "\b", 'f' => "\f", 'n' => "\n",
77
+ 'r' => "\r", 't' => "\t", 'v' => "\v"
78
+ }.freeze
79
+
80
+ SYMBOLS = {
81
+ ',' => :comma, '/' => :slash, '*' => :asterisk, '+' => :plus,
82
+ '(' => :left_paren, ')' => :right_paren,
83
+ '[' => :left_bracket, ']' => :right_bracket,
84
+ '{' => :left_curly, '}' => :right_curly
85
+ }.freeze
86
+
87
+ def initialize(source)
88
+ raise ArgumentError, 'source must be a String' unless source.is_a?(String)
89
+
90
+ @chars = source.chars
91
+ @pos = 0
92
+ @line = 1
93
+ @column = 1
94
+ end
95
+
96
+ def tokenize
97
+ tokens = []
98
+ loop do
99
+ skip_whitespace_and_comments
100
+ start_line, start_column = @line, @column
101
+ if at_end?
102
+ tokens << Token.new(:eof, nil, line: start_line, column: start_column)
103
+ break
104
+ end
105
+ tokens << next_token(start_line, start_column)
106
+ end
107
+ tokens.freeze
108
+ end
109
+
110
+ private
111
+
112
+ def at_end?
113
+ @pos >= @chars.length
114
+ end
115
+
116
+ def peek(offset = 0)
117
+ @chars[@pos + offset]
118
+ end
119
+
120
+ def advance
121
+ c = @chars[@pos]
122
+ @pos += 1
123
+ if c == "\n"
124
+ @line += 1
125
+ @column = 1
126
+ else
127
+ @column += 1
128
+ end
129
+ c
130
+ end
131
+
132
+ def remaining
133
+ @chars[@pos..].join
134
+ end
135
+
136
+ def skip_whitespace_and_comments
137
+ loop do
138
+ if !at_end? && peek =~ /[ \t\r\n]/
139
+ advance
140
+ elsif peek == '-' && peek(1) == '-' && comment_ahead?
141
+ skip_comment
142
+ else
143
+ break
144
+ end
145
+ end
146
+ end
147
+
148
+ # A "--" starts a COMMENT only when followed by a space (a
149
+ # "-- text" line comment) or immediately by newline/EOF (an empty
150
+ # comment). Any other following character means the "--" itself is
151
+ # a SYM_DOUBLE_DASH token, handled by next_token instead.
152
+ def comment_ahead?
153
+ after = peek(2)
154
+ after.nil? || after == ' ' || after == "\n" || after == "\r"
155
+ end
156
+
157
+ def skip_comment
158
+ advance # first '-'
159
+ advance # second '-'
160
+ advance while !at_end? && peek != "\n"
161
+ end
162
+
163
+ def next_token(line, column)
164
+ c = peek
165
+ return read_string(line, column) if c == "'" || c == '"'
166
+ return read_parameter(line, column) if c == '$'
167
+ return read_number(line, column) if c =~ /\d/
168
+ return read_word(line, column) if c =~ /[A-Za-z]/
169
+ return read_double_dash_or_symbol(line, column) if c == '-'
170
+ return read_comparison_operator(line, column) if c =~ /[=!<>]/
171
+ return read_symbol(line, column) if SYMBOLS.key?(c)
172
+
173
+ raise ParseError.new("unexpected character #{c.inspect}", line: line, column: column)
174
+ end
175
+
176
+ def read_word(line, column)
177
+ hrid = ARCHETYPE_HRID_RE.match(remaining)&.to_s
178
+ at_id = AT_ID_CODE_RE.match(remaining)&.to_s
179
+ uri = URI_RE.match(remaining)&.to_s
180
+ plain = IDENTIFIER_RE.match(remaining)&.to_s
181
+
182
+ candidates = [
183
+ [hrid, :archetype_hrid, 0],
184
+ [at_id, :at_id_code, 1],
185
+ [uri, :uri, 2],
186
+ [plain, :plain_word, 3]
187
+ ].select { |text, _type, _pri| text }
188
+ text, kind, = candidates.max_by { |t, _k, pri| [t.length, -pri] }
189
+
190
+ text.length.times { advance }
191
+
192
+ case kind
193
+ when :archetype_hrid
194
+ Token.new(:archetype_hrid, text, line: line, column: column)
195
+ when :at_id_code
196
+ type = text.start_with?('at') ? :at_code : :id_code
197
+ Token.new(type, text, line: line, column: column)
198
+ when :uri
199
+ Token.new(:uri, text, line: line, column: column)
200
+ else
201
+ build_word_token(text, line, column)
202
+ end
203
+ end
204
+
205
+ def build_word_token(text, line, column)
206
+ downcased = text.downcase
207
+ if downcased == 'true' || downcased == 'false'
208
+ Token.new(:boolean, downcased == 'true', line: line, column: column)
209
+ elsif KEYWORDS.key?(downcased)
210
+ Token.new(KEYWORDS[downcased], text, line: line, column: column)
211
+ else
212
+ Token.new(:identifier, text, line: line, column: column)
213
+ end
214
+ end
215
+
216
+ def read_number(line, column)
217
+ text = NUMBER_RE.match(remaining).to_s
218
+ text.length.times { advance }
219
+ real = text.include?('.')
220
+ sci = text =~ /[eE]/
221
+ type = if real && sci
222
+ :sci_real
223
+ elsif real
224
+ :real
225
+ elsif sci
226
+ :sci_integer
227
+ else
228
+ :integer
229
+ end
230
+ value = real || sci ? Float(text) : Integer(text)
231
+ Token.new(type, value, line: line, column: column)
232
+ end
233
+
234
+ def read_string(line, column)
235
+ quote = advance
236
+ buffer = +''
237
+ loop do
238
+ raise ParseError.new('unterminated string literal', line: line, column: column) if at_end?
239
+
240
+ ch = advance
241
+ if ch == quote
242
+ break
243
+ elsif ch == '\\'
244
+ raise ParseError.new('unterminated string literal', line: line, column: column) if at_end?
245
+
246
+ esc = advance
247
+ buffer << (ESCAPES[esc] || esc)
248
+ else
249
+ buffer << ch
250
+ end
251
+ end
252
+ Token.new(:string, buffer, line: line, column: column)
253
+ end
254
+
255
+ def read_parameter(line, column)
256
+ advance # '$'
257
+ name = IDENTIFIER_RE.match(remaining)&.to_s
258
+ if name.nil? || name.empty?
259
+ raise ParseError.new('expected an identifier after $', line: line, column: column)
260
+ end
261
+
262
+ name.length.times { advance }
263
+ Token.new(:parameter, name, line: line, column: column)
264
+ end
265
+
266
+ def read_double_dash_or_symbol(line, column)
267
+ if peek(1) == '-'
268
+ advance
269
+ advance
270
+ Token.new(:double_dash, '--', line: line, column: column)
271
+ else
272
+ advance
273
+ Token.new(:minus, '-', line: line, column: column)
274
+ end
275
+ end
276
+
277
+ def read_comparison_operator(line, column)
278
+ c = advance
279
+ if (c == '!' || c == '<' || c == '>') && peek == '='
280
+ advance
281
+ Token.new(:comparison_operator, "#{c}=", line: line, column: column)
282
+ elsif c == '!'
283
+ raise ParseError.new("unexpected character \"!\"", line: line, column: column)
284
+ else
285
+ Token.new(:comparison_operator, c, line: line, column: column)
286
+ end
287
+ end
288
+
289
+ def read_symbol(line, column)
290
+ c = advance
291
+ Token.new(SYMBOLS.fetch(c), c, line: line, column: column)
292
+ end
293
+ end
294
+ end
295
+ end
@@ -0,0 +1,45 @@
1
+ module OpenEHR
2
+ module AQL
3
+ module Model
4
+ # containsExpr's "classExprOperand (NOT? CONTAINS containsExpr)?"
5
+ # shape: a right-recursive chain (A CONTAINS B CONTAINS C ...), so
6
+ # `child` is itself a ClassExpression, another Containment, or (from
7
+ # M7) a ContainmentAnd/ContainmentOr grouping of sibling branches.
8
+ class Containment
9
+ attr_reader :parent, :child, :negated
10
+ alias negated? negated
11
+
12
+ def initialize(parent:, child:, negated: false)
13
+ @parent = parent
14
+ @child = child
15
+ @negated = negated
16
+ freeze
17
+ end
18
+ end
19
+
20
+ # containsExpr : ... | containsExpr AND containsExpr | ... ;
21
+ # Groups sibling containment branches that must ALL be present
22
+ # under the same parent, e.g. "CONTAINS (OBSERVATION o1 AND OBSERVATION o2)".
23
+ class ContainmentAnd
24
+ attr_reader :left, :right
25
+
26
+ def initialize(left:, right:)
27
+ @left = left
28
+ @right = right
29
+ freeze
30
+ end
31
+ end
32
+
33
+ # containsExpr : ... | containsExpr OR containsExpr | ... ;
34
+ class ContainmentOr
35
+ attr_reader :left, :right
36
+
37
+ def initialize(left:, right:)
38
+ @left = left
39
+ @right = right
40
+ freeze
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,35 @@
1
+ module OpenEHR
2
+ module AQL
3
+ module Model
4
+ # fromClause : FROM fromExpr ; fromExpr : containsExpr ;
5
+ # `containment` holds the root of the containsExpr tree. Until M3/M7
6
+ # add archetype/node predicates and CONTAINS nesting, it is always a
7
+ # single, unpredicated ClassExpression.
8
+ class FromClause
9
+ attr_reader :containment
10
+
11
+ def initialize(containment:)
12
+ @containment = containment
13
+ freeze
14
+ end
15
+ end
16
+
17
+ # classExprOperand's #classExpression alternative:
18
+ # IDENTIFIER variable=IDENTIFIER? pathPredicate?
19
+ # A bare RM class name (e.g. "COMPOSITION"), optionally bound to a
20
+ # variable and constrained by a predicate. Until M3 adds
21
+ # archetypePredicate/nodePredicate, `predicate` can only be a
22
+ # StandardPredicate (or nil).
23
+ class ClassExpression
24
+ attr_reader :class_name, :variable, :predicate
25
+
26
+ def initialize(class_name:, variable: nil, predicate: nil)
27
+ @class_name = class_name
28
+ @variable = variable
29
+ @predicate = predicate
30
+ freeze
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,56 @@
1
+ module OpenEHR
2
+ module AQL
3
+ module Model
4
+ # aggregateFunctionCall : name=COUNT SYM_LEFT_PAREN (DISTINCT? identifiedPath | SYM_ASTERISK) SYM_RIGHT_PAREN
5
+ # | name=(MIN | MAX | SUM | AVG) SYM_LEFT_PAREN identifiedPath SYM_RIGHT_PAREN ;
6
+ # `path` is nil only for the COUNT(*) form; `distinct` is only ever
7
+ # true for COUNT (the grammar doesn't allow DISTINCT on MIN/MAX/SUM/AVG).
8
+ class AggregateFunctionCall
9
+ attr_reader :name, :path, :distinct
10
+
11
+ def initialize(name:, path: nil, distinct: false)
12
+ @name = name
13
+ @path = path
14
+ @distinct = distinct
15
+ freeze
16
+ end
17
+ end
18
+
19
+ # functionCall : terminologyFunction
20
+ # | name=(STRING_FUNCTION_ID | NUMERIC_FUNCTION_ID | DATE_TIME_FUNCTION_ID | IDENTIFIER)
21
+ # SYM_LEFT_PAREN (terminal (SYM_COMMA terminal)*)? SYM_RIGHT_PAREN ;
22
+ class FunctionCall
23
+ attr_reader :name, :arguments
24
+
25
+ def initialize(name:, arguments: [])
26
+ @name = name
27
+ @arguments = arguments.freeze
28
+ freeze
29
+ end
30
+ end
31
+
32
+ # terminologyFunction : TERMINOLOGY SYM_LEFT_PAREN STRING SYM_COMMA STRING SYM_COMMA STRING SYM_RIGHT_PAREN ;
33
+ # The grammar doesn't name the three STRING arguments (their meaning
34
+ # - expand-operation, terminology id, value-set expression - is
35
+ # spec prose, not grammar), so they're kept as a plain positional list.
36
+ class TerminologyFunctionCall
37
+ attr_reader :args
38
+
39
+ def initialize(args:)
40
+ @args = args.freeze
41
+ freeze
42
+ end
43
+ end
44
+
45
+ # matchesOperand's bare-URI alternative: SYM_LEFT_CURLY URI SYM_RIGHT_CURLY ;
46
+ class UriRef
47
+ attr_reader :uri
48
+
49
+ def initialize(uri:)
50
+ @uri = uri
51
+ freeze
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,20 @@
1
+ module OpenEHR
2
+ module AQL
3
+ module Model
4
+ # identifiedPath : IDENTIFIER pathPredicate? (SYM_SLASH objectPath)? ;
5
+ # A variable reference, optionally constrained by a predicate on the
6
+ # variable itself (e.g. "c[at001 or at002]"), optionally followed by
7
+ # an object path rooted at that variable (e.g. "o/data[at0001]/.../value").
8
+ class IdentifiedPath
9
+ attr_reader :variable, :predicate, :path
10
+
11
+ def initialize(variable:, predicate: nil, path: nil)
12
+ @variable = variable
13
+ @predicate = predicate
14
+ @path = path
15
+ freeze
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ module OpenEHR
2
+ module AQL
3
+ module Model
4
+ # primitive : STRING | numericPrimitive | DATE | TIME | DATETIME | BOOLEAN | NULL ;
5
+ class Literal
6
+ attr_reader :value
7
+
8
+ def initialize(value:)
9
+ @value = value
10
+ freeze
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,34 @@
1
+ module OpenEHR
2
+ module AQL
3
+ module Model
4
+ # objectPath : pathPart (SYM_SLASH pathPart)* ;
5
+ class ObjectPath
6
+ attr_reader :segments
7
+
8
+ def initialize(segments:)
9
+ @segments = segments.freeze
10
+ freeze
11
+ end
12
+
13
+ def to_s
14
+ segments.map(&:to_s).join('/')
15
+ end
16
+ end
17
+
18
+ # pathPart : IDENTIFIER pathPredicate? ;
19
+ class PathPart
20
+ attr_reader :attribute, :predicate
21
+
22
+ def initialize(attribute:, predicate: nil)
23
+ @attribute = attribute
24
+ @predicate = predicate
25
+ freeze
26
+ end
27
+
28
+ def to_s
29
+ predicate ? "#{attribute}[#{predicate}]" : attribute
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,50 @@
1
+ module OpenEHR
2
+ module AQL
3
+ module Model
4
+ # orderByClause : ORDER BY orderByExpr (SYM_COMMA orderByExpr)* ;
5
+ class OrderByClause
6
+ attr_reader :items
7
+
8
+ def initialize(items:)
9
+ @items = items.freeze
10
+ freeze
11
+ end
12
+ end
13
+
14
+ # orderByExpr : identifiedPath order=(DESCENDING|DESC|ASCENDING|ASC)? ;
15
+ # `direction` is always :asc or :desc - the grammar's four spellings
16
+ # (ASC/ASCENDING/DESC/DESCENDING) collapse to one of these two.
17
+ class OrderByItem
18
+ attr_reader :path, :direction
19
+
20
+ def initialize(path:, direction: :asc)
21
+ @path = path
22
+ @direction = direction
23
+ freeze
24
+ end
25
+ end
26
+
27
+ # limitClause : LIMIT limit=INTEGER (OFFSET offset=INTEGER)? ;
28
+ class LimitClause
29
+ attr_reader :limit, :offset
30
+
31
+ def initialize(limit:, offset: 0)
32
+ @limit = limit
33
+ @offset = offset
34
+ freeze
35
+ end
36
+ end
37
+
38
+ # top : TOP INTEGER direction=(FORWARD|BACKWARD)? ; (deprecated)
39
+ class Top
40
+ attr_reader :count, :direction
41
+
42
+ def initialize(count:, direction: nil)
43
+ @count = count
44
+ @direction = direction
45
+ freeze
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,84 @@
1
+ module OpenEHR
2
+ module AQL
3
+ module Model
4
+ # standardPredicate : objectPath COMPARISON_OPERATOR pathPredicateOperand ;
5
+ # (nodePredicate's 4th alternative has this identical shape - see
6
+ # Parser#parse_standard_predicate's comment.)
7
+ class StandardPredicate
8
+ attr_reader :path, :operator, :operand
9
+
10
+ def initialize(path:, operator:, operand:)
11
+ @path = path
12
+ @operator = operator
13
+ @operand = operand
14
+ freeze
15
+ end
16
+ end
17
+
18
+ # archetypePredicate : ARCHETYPE_HRID (SYM_COMMA (STRING | PARAMETER | TERM_CODE | AT_CODE | ID_CODE))? | PARAMETER ;
19
+ # `value` is the optional SYM_COMMA suffix: a raw String (for
20
+ # STRING/AT_CODE/ID_CODE) or a Parameter. The bare-PARAMETER
21
+ # alternative (a whole predicate substituted at execution time,
22
+ # e.g. "[$archetypeId]") isn't needed by any current example and is
23
+ # deferred - see Parser#parse_predicate_primary.
24
+ class ArchetypePredicate
25
+ attr_reader :archetype_id, :value
26
+
27
+ def initialize(archetype_id:, value: nil)
28
+ @archetype_id = archetype_id
29
+ @value = value
30
+ freeze
31
+ end
32
+ end
33
+
34
+ # nodePredicate : (ID_CODE | AT_CODE) (SYM_COMMA (STRING | PARAMETER | TERM_CODE | AT_CODE | ID_CODE))? | ... ;
35
+ # `value` is the optional SYM_COMMA suffix, same shape as
36
+ # ArchetypePredicate#value.
37
+ class NodePredicate
38
+ attr_reader :code, :value
39
+
40
+ def initialize(code:, value: nil)
41
+ @code = code
42
+ @value = value
43
+ freeze
44
+ end
45
+ end
46
+
47
+ # nodePredicate : ... | nodePredicate AND nodePredicate | ... ;
48
+ # Combines any pathPredicate-body primaries (ArchetypePredicate,
49
+ # NodePredicate, Parameter, StandardPredicate). AND binds tighter
50
+ # than OR, same precedence-climbing convention used for whereExpr
51
+ # (M5) and containsExpr (M7).
52
+ class PredicateAnd
53
+ attr_reader :left, :right
54
+
55
+ def initialize(left:, right:)
56
+ @left = left
57
+ @right = right
58
+ freeze
59
+ end
60
+ end
61
+
62
+ # nodePredicate : ... | nodePredicate OR nodePredicate ;
63
+ class PredicateOr
64
+ attr_reader :left, :right
65
+
66
+ def initialize(left:, right:)
67
+ @left = left
68
+ @right = right
69
+ freeze
70
+ end
71
+ end
72
+
73
+ # PARAMETER: '$' IDENTIFIER_CHAR ;
74
+ class Parameter
75
+ attr_reader :name
76
+
77
+ def initialize(name:)
78
+ @name = name
79
+ freeze
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,24 @@
1
+ module OpenEHR
2
+ module AQL
3
+ module Model
4
+ # The parsed, immutable representation of an AQL selectQuery.
5
+ # Clauses that a milestone hasn't implemented yet are nil.
6
+ class Query
7
+ attr_reader :select_clause, :from_clause, :where_clause, :order_by_clause, :limit_clause
8
+
9
+ def initialize(select_clause:, from_clause:, where_clause: nil, order_by_clause: nil, limit_clause: nil)
10
+ @select_clause = select_clause
11
+ @from_clause = from_clause
12
+ @where_clause = where_clause
13
+ @order_by_clause = order_by_clause
14
+ @limit_clause = limit_clause
15
+ freeze
16
+ end
17
+
18
+ def execute(dataset, params: {})
19
+ OpenEHR::AQL::Engine.new(self).execute(dataset, params: params)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,29 @@
1
+ module OpenEHR
2
+ module AQL
3
+ module Model
4
+ # selectClause : SELECT DISTINCT? top? selectExpr (SYM_COMMA selectExpr)* ;
5
+ class SelectClause
6
+ attr_reader :columns, :distinct, :top
7
+ alias distinct? distinct
8
+
9
+ def initialize(columns:, distinct: false, top: nil)
10
+ @columns = columns.freeze
11
+ @distinct = distinct
12
+ @top = top
13
+ freeze
14
+ end
15
+ end
16
+
17
+ # selectExpr : columnExpr (AS aliasName=IDENTIFIER)? ;
18
+ class SelectColumn
19
+ attr_reader :expression, :alias_name
20
+
21
+ def initialize(expression:, alias_name: nil)
22
+ @expression = expression
23
+ @alias_name = alias_name
24
+ freeze
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end