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,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
|
data/lib/openehr/aql.rb
ADDED
|
@@ -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
|