openehr 1.3.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.rdoc +93 -12
- data/lib/openehr/am/archetype/constraint_model/primitive.rb +204 -6
- data/lib/openehr/am/archetype/constraint_model.rb +228 -4
- data/lib/openehr/am/archetype/ontology.rb +2 -2
- data/lib/openehr/am/archetype.rb +101 -2
- data/lib/openehr/am/openehr_profile/data_types/basic.rb +28 -0
- data/lib/openehr/am/openehr_profile/data_types/quantity.rb +88 -0
- data/lib/openehr/am/openehr_profile/data_types/text.rb +23 -0
- data/lib/openehr/am/template.rb +25 -3
- data/lib/openehr/aql/engine/binding.rb +13 -0
- data/lib/openehr/aql/engine/contains_resolver.rb +161 -0
- data/lib/openehr/aql/engine/dataset.rb +122 -0
- data/lib/openehr/aql/engine/path_evaluator.rb +137 -0
- data/lib/openehr/aql/engine/predicate_evaluator.rb +65 -0
- data/lib/openehr/aql/engine.rb +119 -0
- data/lib/openehr/aql/errors.rb +27 -0
- data/lib/openehr/aql/lexer.rb +295 -0
- data/lib/openehr/aql/model/containment.rb +45 -0
- data/lib/openehr/aql/model/from_clause.rb +35 -0
- data/lib/openehr/aql/model/function_call.rb +56 -0
- data/lib/openehr/aql/model/identified_path.rb +20 -0
- data/lib/openehr/aql/model/literal.rb +15 -0
- data/lib/openehr/aql/model/object_path.rb +34 -0
- data/lib/openehr/aql/model/order_by_and_limit.rb +50 -0
- data/lib/openehr/aql/model/predicate.rb +84 -0
- data/lib/openehr/aql/model/query.rb +24 -0
- data/lib/openehr/aql/model/select_clause.rb +29 -0
- data/lib/openehr/aql/model/where_clause.rb +105 -0
- data/lib/openehr/aql/model.rb +14 -0
- data/lib/openehr/aql/parser.rb +484 -0
- data/lib/openehr/aql/result_set.rb +54 -0
- data/lib/openehr/aql.rb +28 -0
- data/lib/openehr/assumed_library_types.rb +68 -17
- data/lib/openehr/parser/adl_grammar.tt +29 -13
- data/lib/openehr/parser/adl_parser.rb +18 -4
- data/lib/openehr/parser/archetype_validator.rb +126 -0
- data/lib/openehr/parser/exception.rb +13 -0
- data/lib/openehr/parser/opt_parser.rb +162 -9
- data/lib/openehr/parser.rb +2 -0
- data/lib/openehr/path.rb +195 -0
- data/lib/openehr/rm/common/archetyped.rb +116 -6
- data/lib/openehr/rm/common/change_control.rb +3 -8
- data/lib/openehr/rm/common/directory.rb +4 -0
- data/lib/openehr/rm/common/generic.rb +24 -10
- data/lib/openehr/rm/composition/content/entry.rb +24 -5
- data/lib/openehr/rm/composition/content/navigation.rb +2 -1
- data/lib/openehr/rm/composition.rb +19 -2
- data/lib/openehr/rm/data_structures/history.rb +3 -0
- data/lib/openehr/rm/data_structures/item_structure/representation.rb +13 -9
- data/lib/openehr/rm/data_structures/item_structure.rb +28 -15
- data/lib/openehr/rm/data_types/encapsulated.rb +23 -3
- data/lib/openehr/rm/data_types/quantity/date_time.rb +9 -1
- data/lib/openehr/rm/data_types/quantity.rb +66 -14
- data/lib/openehr/rm/data_types/text.rb +8 -0
- data/lib/openehr/rm/data_types/time_specification.rb +5 -4
- data/lib/openehr/rm/demographic.rb +4 -3
- data/lib/openehr/rm/ehr.rb +20 -1
- data/lib/openehr/rm/factory.rb +234 -6
- data/lib/openehr/rm/integration.rb +1 -0
- data/lib/openehr/rm/support/identification.rb +37 -8
- data/lib/openehr/rm/support/measurement.rb +32 -0
- data/lib/openehr/rm/type_name.rb +90 -0
- data/lib/openehr/rm.rb +3 -0
- data/lib/openehr/serializer/adl_serializer.rb +335 -0
- data/lib/openehr/serializer/base.rb +20 -0
- data/lib/openehr/serializer/opt_serializer.rb +49 -0
- data/lib/openehr/serializer/rm_json_serializer.rb +62 -0
- data/lib/openehr/serializer/xml_serializer.rb +260 -0
- data/lib/openehr/serializer.rb +5 -291
- data/lib/openehr/terminology_service.rb +44 -0
- data/lib/openehr/version.rb +1 -1
- data/lib/openehr.rb +5 -2
- metadata +35 -7
- data/lib/openehr/parser/validator.rb +0 -18
- data/lib/openehr/parser/xml_parser.rb +0 -13
- data/lib/openehr/rm/data_types/charset_extract.rb +0 -24
- data/lib/openehr/writer.rb +0 -12
|
@@ -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
|