fhirpath 0.2.0.pre1
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 +7 -0
- data/.rubocop.yml +73 -0
- data/CHANGELOG.md +77 -0
- data/CONTRIBUTING.md +67 -0
- data/Gemfile +12 -0
- data/LICENSE +21 -0
- data/README.md +218 -0
- data/Rakefile +19 -0
- data/SECURITY.md +20 -0
- data/conformance/core.jsonl +39 -0
- data/conformance/official-r4-core.json +15 -0
- data/conformance/r4.jsonl +6 -0
- data/docs/api.md +170 -0
- data/docs/architecture.md +507 -0
- data/docs/conformance.md +67 -0
- data/docs/feature-matrix.md +49 -0
- data/docs/first-slice.md +57 -0
- data/docs/release-checklist.md +57 -0
- data/docs/releasing.md +87 -0
- data/docs/support-matrix.md +102 -0
- data/lib/fhirpath/ast.rb +128 -0
- data/lib/fhirpath/capability.rb +57 -0
- data/lib/fhirpath/collection.rb +94 -0
- data/lib/fhirpath/compiled_expression.rb +35 -0
- data/lib/fhirpath/conformance/importer.rb +317 -0
- data/lib/fhirpath/errors.rb +76 -0
- data/lib/fhirpath/evaluation_context.rb +30 -0
- data/lib/fhirpath/evaluator.rb +690 -0
- data/lib/fhirpath/functions.rb +71 -0
- data/lib/fhirpath/host_services.rb +52 -0
- data/lib/fhirpath/model.rb +47 -0
- data/lib/fhirpath/model_registry.rb +17 -0
- data/lib/fhirpath/models_r4.rb +91 -0
- data/lib/fhirpath/parser.rb +487 -0
- data/lib/fhirpath/source_span.rb +25 -0
- data/lib/fhirpath/types.rb +73 -0
- data/lib/fhirpath/vector_runner.rb +130 -0
- data/lib/fhirpath/version.rb +9 -0
- data/lib/fhirpath.rb +77 -0
- data/script/import_vectors.rb +14 -0
- data/script/run_vectors.rb +19 -0
- metadata +120 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bigdecimal'
|
|
4
|
+
|
|
5
|
+
module FHIRPath
|
|
6
|
+
class Token
|
|
7
|
+
attr_reader :type, :value, :span
|
|
8
|
+
|
|
9
|
+
def initialize(type:, value:, span:)
|
|
10
|
+
@type = type
|
|
11
|
+
@value = value
|
|
12
|
+
@span = span
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class Lexer
|
|
18
|
+
SINGLE = {
|
|
19
|
+
'.' => :dot, '[' => :left_bracket, ']' => :right_bracket,
|
|
20
|
+
'(' => :left_paren, ')' => :right_paren, ',' => :comma,
|
|
21
|
+
'{' => :left_brace, '}' => :right_brace,
|
|
22
|
+
'=' => :equals, '+' => :plus, '-' => :minus,
|
|
23
|
+
'*' => :multiply, '/' => :divide,
|
|
24
|
+
'<' => :less_than, '>' => :greater_than,
|
|
25
|
+
'|' => :union, '&' => :concatenate, '~' => :equivalent
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
TWO_CHARACTER = {
|
|
29
|
+
'!=' => :not_equals, '<=' => :less_or_equal, '>=' => :greater_or_equal,
|
|
30
|
+
'!~' => :not_equivalent
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
KEYWORD_OPERATORS = %w[and or xor implies div mod in contains is as].freeze
|
|
34
|
+
|
|
35
|
+
def initialize(source)
|
|
36
|
+
@source = source
|
|
37
|
+
@index = 0
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def tokens
|
|
41
|
+
result = []
|
|
42
|
+
until eof?
|
|
43
|
+
skip_ignored
|
|
44
|
+
break if eof?
|
|
45
|
+
|
|
46
|
+
result << next_token
|
|
47
|
+
end
|
|
48
|
+
result << Token.new(type: :eof, value: nil,
|
|
49
|
+
span: SourceSpan.new(offset: @index, length: 0))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def eof?
|
|
55
|
+
@index >= @source.length
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def skip_ignored
|
|
59
|
+
loop do
|
|
60
|
+
@index += 1 while !eof? && @source[@index] =~ /\s/
|
|
61
|
+
return if eof?
|
|
62
|
+
|
|
63
|
+
if @source[@index, 2] == '//'
|
|
64
|
+
@index += 2
|
|
65
|
+
@index += 1 while !eof? && @source[@index] != "\n"
|
|
66
|
+
elsif @source[@index, 2] == '/*'
|
|
67
|
+
start = @index
|
|
68
|
+
closing = @source.index('*/', @index + 2)
|
|
69
|
+
raise_error('unterminated block comment', start) unless closing
|
|
70
|
+
|
|
71
|
+
@index = closing + 2
|
|
72
|
+
else
|
|
73
|
+
return
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def next_token
|
|
79
|
+
start = @index
|
|
80
|
+
two_character = @source[start, 2]
|
|
81
|
+
if TWO_CHARACTER.key?(two_character)
|
|
82
|
+
@index += 2
|
|
83
|
+
return token(TWO_CHARACTER.fetch(two_character), two_character, start)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
char = @source[@index]
|
|
87
|
+
if SINGLE.key?(char)
|
|
88
|
+
@index += 1
|
|
89
|
+
return token(SINGLE.fetch(char), char, start)
|
|
90
|
+
end
|
|
91
|
+
return string_token(start) if char == "'"
|
|
92
|
+
return external_token(start) if char == '%'
|
|
93
|
+
return variable_token(start) if char == '$'
|
|
94
|
+
return number_token(start) if char =~ /[0-9]/
|
|
95
|
+
return identifier_token(start) if char =~ /[A-Za-z_]/
|
|
96
|
+
|
|
97
|
+
raise_error("unexpected character #{char.inspect}", start)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def string_token(start)
|
|
101
|
+
@index += 1
|
|
102
|
+
value = +''
|
|
103
|
+
until eof?
|
|
104
|
+
char = @source[@index]
|
|
105
|
+
@index += 1
|
|
106
|
+
return token(:string, value, start) if char == "'"
|
|
107
|
+
|
|
108
|
+
if char == '\\'
|
|
109
|
+
raise_error('unterminated string escape', @index - 1) if eof?
|
|
110
|
+
|
|
111
|
+
value << escaped_character
|
|
112
|
+
else
|
|
113
|
+
value << char
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
raise_error('unterminated string literal', start)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def escaped_character
|
|
120
|
+
escaped = @source[@index]
|
|
121
|
+
@index += 1
|
|
122
|
+
simple = { 'b' => "\b", 'f' => "\f", 'n' => "\n", 'r' => "\r",
|
|
123
|
+
't' => "\t", '\\' => '\\', "'" => "'", '/' => '/' }
|
|
124
|
+
return simple.fetch(escaped) if simple.key?(escaped)
|
|
125
|
+
return unicode_escape if escaped == 'u'
|
|
126
|
+
|
|
127
|
+
raise_error("unsupported string escape \\#{escaped}", @index - 1, code: :invalid_escape)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def unicode_escape
|
|
131
|
+
length = 4
|
|
132
|
+
digits = @source[@index, length]
|
|
133
|
+
unless digits&.match?(/\A[0-9A-Fa-f]{#{length}}\z/)
|
|
134
|
+
raise_error('invalid unicode escape', @index - 1, code: :invalid_escape)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
@index += length
|
|
138
|
+
codepoint = digits.to_i(16)
|
|
139
|
+
if codepoint.between?(0xD800, 0xDBFF)
|
|
140
|
+
unless @source[@index, 2] == '\\u'
|
|
141
|
+
raise_error('high surrogate must be followed by a low surrogate', @index, code: :invalid_escape)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
@index += 2
|
|
145
|
+
low_digits = @source[@index, length]
|
|
146
|
+
unless low_digits&.match?(/\A[0-9A-Fa-f]{#{length}}\z/)
|
|
147
|
+
raise_error('invalid unicode surrogate pair', @index - 2, code: :invalid_escape)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
low = low_digits.to_i(16)
|
|
151
|
+
unless low.between?(0xDC00, 0xDFFF)
|
|
152
|
+
raise_error('high surrogate must be followed by a low surrogate', @index, code: :invalid_escape)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
@index += length
|
|
156
|
+
codepoint = 0x10000 + ((codepoint - 0xD800) * 0x400) + (low - 0xDC00)
|
|
157
|
+
elsif codepoint.between?(0xDC00, 0xDFFF)
|
|
158
|
+
raise_error('low surrogate must follow a high surrogate', @index - length, code: :invalid_escape)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
[codepoint].pack('U')
|
|
162
|
+
rescue RangeError
|
|
163
|
+
raise_error('invalid unicode code point', @index - length, code: :invalid_escape)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def external_token(start)
|
|
167
|
+
@index += 1
|
|
168
|
+
name_start = @index
|
|
169
|
+
@index += 1 while !eof? && @source[@index] =~ /[A-Za-z0-9_]/
|
|
170
|
+
raise_error('external constant requires a name', start) if name_start == @index
|
|
171
|
+
|
|
172
|
+
token(:external, @source[name_start...@index], start)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def variable_token(start)
|
|
176
|
+
@index += 1
|
|
177
|
+
name_start = @index
|
|
178
|
+
@index += 1 while !eof? && @source[@index] =~ /[A-Za-z0-9_]/
|
|
179
|
+
raise_error('variable requires a name', start) if name_start == @index
|
|
180
|
+
|
|
181
|
+
token(:variable, @source[name_start...@index], start)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def number_token(start)
|
|
185
|
+
@index += 1 while !eof? && @source[@index] =~ /[0-9]/
|
|
186
|
+
type = :integer
|
|
187
|
+
if !eof? && @source[@index] == '.' && @source[@index + 1] =~ /[0-9]/
|
|
188
|
+
type = :decimal
|
|
189
|
+
@index += 1
|
|
190
|
+
@index += 1 while !eof? && @source[@index] =~ /[0-9]/
|
|
191
|
+
end
|
|
192
|
+
if !eof? && @source[@index] =~ /[eE]/
|
|
193
|
+
type = :decimal
|
|
194
|
+
@index += 1
|
|
195
|
+
@index += 1 if !eof? && @source[@index] =~ /[+-]/
|
|
196
|
+
exponent_start = @index
|
|
197
|
+
@index += 1 while !eof? && @source[@index] =~ /[0-9]/
|
|
198
|
+
raise_error('decimal exponent requires digits', exponent_start) if exponent_start == @index
|
|
199
|
+
end
|
|
200
|
+
text = @source[start...@index]
|
|
201
|
+
value = type == :integer ? text.to_i : BigDecimal(text)
|
|
202
|
+
token(type, value, start)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def identifier_token(start)
|
|
206
|
+
@index += 1 while !eof? && @source[@index] =~ /[A-Za-z0-9_]/
|
|
207
|
+
text = @source[start...@index]
|
|
208
|
+
return token(:operator, text.to_sym, start) if KEYWORD_OPERATORS.include?(text)
|
|
209
|
+
|
|
210
|
+
token(:identifier, text, start)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def token(type, value, start)
|
|
214
|
+
Token.new(type: type, value: value,
|
|
215
|
+
span: SourceSpan.new(offset: start, length: @index - start))
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def raise_error(message, start, code: :invalid_token)
|
|
219
|
+
raise ParseError.new(message, code: code,
|
|
220
|
+
span: SourceSpan.new(offset: start, length: [@index - start, 1].max),
|
|
221
|
+
expression: @source)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
class ParsedExpression
|
|
226
|
+
# Maximum AST depth allowed when building the source map. This is the same
|
|
227
|
+
# budget used by the Parser for recursion; keeping both in sync ensures a
|
|
228
|
+
# parse that succeeds will not then overflow during source-map traversal.
|
|
229
|
+
MAX_NESTING_DEPTH = 256
|
|
230
|
+
|
|
231
|
+
attr_reader :source, :ast, :source_map
|
|
232
|
+
|
|
233
|
+
def initialize(source:, ast:)
|
|
234
|
+
@source = source.freeze
|
|
235
|
+
@ast = ast
|
|
236
|
+
@source_map = build_source_map(ast).freeze
|
|
237
|
+
freeze
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
private
|
|
241
|
+
|
|
242
|
+
def build_source_map(node, result = {}, depth = 0)
|
|
243
|
+
return result unless node.is_a?(AST::Node)
|
|
244
|
+
|
|
245
|
+
if depth > MAX_NESTING_DEPTH
|
|
246
|
+
raise ParseError.new(
|
|
247
|
+
"expression nesting exceeds the maximum depth of #{MAX_NESTING_DEPTH}",
|
|
248
|
+
code: :nesting_depth_exceeded,
|
|
249
|
+
span: node.span,
|
|
250
|
+
expression: @source
|
|
251
|
+
)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
result[node] = node.span
|
|
255
|
+
node.instance_variables.each do |name|
|
|
256
|
+
value = node.instance_variable_get(name)
|
|
257
|
+
if value.is_a?(Array)
|
|
258
|
+
value.each { |child| build_source_map(child, result, depth + 1) }
|
|
259
|
+
else
|
|
260
|
+
build_source_map(value, result, depth + 1)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
result
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
class Parser
|
|
268
|
+
PRECEDENCE = {
|
|
269
|
+
implies: 1,
|
|
270
|
+
or: 2, xor: 2,
|
|
271
|
+
and: 3,
|
|
272
|
+
in: 4, contains: 4,
|
|
273
|
+
equals: 5, not_equals: 5, equivalent: 5, not_equivalent: 5,
|
|
274
|
+
less_than: 6, less_or_equal: 6, greater_than: 6, greater_or_equal: 6,
|
|
275
|
+
union: 7,
|
|
276
|
+
is: 8, as: 8,
|
|
277
|
+
plus: 9, minus: 9, concatenate: 9,
|
|
278
|
+
multiply: 10, divide: 10, div: 10, mod: 10
|
|
279
|
+
}.freeze
|
|
280
|
+
|
|
281
|
+
BINARY_TOKENS = PRECEDENCE.keys.freeze
|
|
282
|
+
|
|
283
|
+
# Maximum recursion depth for the recursive-descent parser. Deeply nested
|
|
284
|
+
# expressions (parentheses, collections, function calls, indexers, unary
|
|
285
|
+
# chains, and deep precedence trees) recurse on the Ruby call stack; without
|
|
286
|
+
# a budget the process can exhaust the stack and raise SystemStackError,
|
|
287
|
+
# escaping the FHIRPath::Error boundary. Beyond this depth parsing fails
|
|
288
|
+
# with a structured ParseError (code :nesting_depth_exceeded) instead.
|
|
289
|
+
MAX_NESTING_DEPTH = 256
|
|
290
|
+
|
|
291
|
+
def self.parse(source, capability: Capability.current)
|
|
292
|
+
new(source, capability: capability).parse
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def initialize(source, capability:)
|
|
296
|
+
# Duplicate so we never retain the caller's own String identity: the
|
|
297
|
+
# compiled program is then immutable via a frozen internal snapshot
|
|
298
|
+
# without freezing the caller-owned source string.
|
|
299
|
+
@source = source.to_s.dup
|
|
300
|
+
@capability = capability
|
|
301
|
+
@tokens = Lexer.new(@source).tokens
|
|
302
|
+
@position = 0
|
|
303
|
+
@nesting_depth = 0
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def parse
|
|
307
|
+
fail_parse('expression cannot be empty', :empty_expression) if current.type == :eof
|
|
308
|
+
|
|
309
|
+
ast = parse_expression(0)
|
|
310
|
+
fail_parse('trailing input after expression', :trailing_input) unless current.type == :eof
|
|
311
|
+
|
|
312
|
+
ParsedExpression.new(source: @source, ast: ast)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
private
|
|
316
|
+
|
|
317
|
+
def parse_expression(min_precedence)
|
|
318
|
+
with_nesting do
|
|
319
|
+
left = parse_unary
|
|
320
|
+
while (precedence = binary_precedence(current)) && precedence >= min_precedence
|
|
321
|
+
operator_token = advance
|
|
322
|
+
right = parse_expression(precedence + 1)
|
|
323
|
+
left = AST::BinaryExpression.new(left: left, operator: operator_for(operator_token),
|
|
324
|
+
right: right,
|
|
325
|
+
span: span_between(left.span, right.span))
|
|
326
|
+
end
|
|
327
|
+
left
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def parse_unary
|
|
332
|
+
return parse_postfix(parse_primary) unless %i[plus minus].include?(current.type)
|
|
333
|
+
|
|
334
|
+
operator = advance
|
|
335
|
+
operand = with_nesting { parse_unary }
|
|
336
|
+
AST::UnaryExpression.new(operator: operator.type,
|
|
337
|
+
operand: operand,
|
|
338
|
+
span: span_between(operator.span, operand.span))
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def parse_primary
|
|
342
|
+
token = advance
|
|
343
|
+
node = case token.type
|
|
344
|
+
when :string, :integer, :decimal
|
|
345
|
+
AST::Literal.new(value: token.value, span: token.span)
|
|
346
|
+
when :identifier
|
|
347
|
+
if %w[true false].include?(token.value)
|
|
348
|
+
AST::Literal.new(value: token.value == 'true', span: token.span)
|
|
349
|
+
else
|
|
350
|
+
AST::Identifier.new(name: token.value, span: token.span)
|
|
351
|
+
end
|
|
352
|
+
when :variable
|
|
353
|
+
AST::Variable.new(name: token.value, span: token.span)
|
|
354
|
+
when :external
|
|
355
|
+
AST::ExternalConstant.new(name: token.value, span: token.span)
|
|
356
|
+
when :left_brace
|
|
357
|
+
parse_collection(token)
|
|
358
|
+
when :left_paren
|
|
359
|
+
inner = parse_expression(0)
|
|
360
|
+
expect(:right_paren)
|
|
361
|
+
inner
|
|
362
|
+
else
|
|
363
|
+
fail_parse("unexpected token #{token.type}", :unexpected_token, token.span)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
parse_postfix(node)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def parse_collection(opening)
|
|
370
|
+
elements = []
|
|
371
|
+
unless current.type == :right_brace
|
|
372
|
+
loop do
|
|
373
|
+
elements << parse_expression(0)
|
|
374
|
+
break unless current.type == :comma
|
|
375
|
+
|
|
376
|
+
advance
|
|
377
|
+
fail_parse('collection item expected after comma', :unexpected_token) if current.type == :right_brace
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
closing = expect(:right_brace)
|
|
381
|
+
AST::CollectionLiteral.new(elements: elements,
|
|
382
|
+
span: span_between(opening.span, closing.span))
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def parse_postfix(node)
|
|
386
|
+
loop do
|
|
387
|
+
case current.type
|
|
388
|
+
when :dot
|
|
389
|
+
advance
|
|
390
|
+
name = expect(:identifier)
|
|
391
|
+
node = if current.type == :left_paren
|
|
392
|
+
parse_function(node, name)
|
|
393
|
+
else
|
|
394
|
+
AST::MemberInvocation.new(receiver: node, name: name.value,
|
|
395
|
+
span: span_between(node.span, name.span))
|
|
396
|
+
end
|
|
397
|
+
when :left_bracket
|
|
398
|
+
advance
|
|
399
|
+
index = parse_expression(0)
|
|
400
|
+
closing = expect(:right_bracket)
|
|
401
|
+
node = AST::Indexer.new(receiver: node, index: index,
|
|
402
|
+
span: span_between(node.span, closing.span))
|
|
403
|
+
when :left_paren
|
|
404
|
+
unless node.is_a?(AST::Identifier)
|
|
405
|
+
fail_parse('only a function name may be invoked', :unexpected_token,
|
|
406
|
+
current.span)
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
node = parse_function(nil, node)
|
|
410
|
+
else
|
|
411
|
+
break
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
node
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def parse_function(receiver, name_token)
|
|
418
|
+
name = name_token.is_a?(AST::Identifier) ? name_token.name : name_token.value
|
|
419
|
+
start = receiver ? receiver.span : name_token.span
|
|
420
|
+
expect(:left_paren)
|
|
421
|
+
arguments = []
|
|
422
|
+
unless current.type == :right_paren
|
|
423
|
+
loop do
|
|
424
|
+
arguments << parse_expression(0)
|
|
425
|
+
break unless current.type == :comma
|
|
426
|
+
|
|
427
|
+
advance
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
closing = expect(:right_paren)
|
|
431
|
+
AST::FunctionInvocation.new(receiver: receiver, name: name,
|
|
432
|
+
arguments: arguments,
|
|
433
|
+
span: span_between(start, closing.span))
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def binary_precedence(token)
|
|
437
|
+
return PRECEDENCE[token.type] if BINARY_TOKENS.include?(token.type)
|
|
438
|
+
return PRECEDENCE[token.value] if token.type == :operator && PRECEDENCE.key?(token.value)
|
|
439
|
+
|
|
440
|
+
nil
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def operator_for(token)
|
|
444
|
+
token.type == :operator ? token.value : token.type
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def expect(type)
|
|
448
|
+
return advance if current.type == type
|
|
449
|
+
|
|
450
|
+
fail_parse("expected #{type}, got #{current.type}", :unexpected_token, current.span)
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def current
|
|
454
|
+
@tokens[@position]
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def advance
|
|
458
|
+
token = current
|
|
459
|
+
@position += 1 unless token.type == :eof
|
|
460
|
+
token
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def span_between(first, last)
|
|
464
|
+
SourceSpan.new(offset: first.offset, length: last.end_offset - first.offset)
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def fail_parse(message, code, span = current.span)
|
|
468
|
+
raise ParseError.new(message, code: code, span: span, expression: @source)
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def with_nesting
|
|
472
|
+
@nesting_depth += 1
|
|
473
|
+
nesting_exceeded if @nesting_depth > MAX_NESTING_DEPTH
|
|
474
|
+
yield
|
|
475
|
+
ensure
|
|
476
|
+
@nesting_depth -= 1
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def nesting_exceeded
|
|
480
|
+
fail_parse(
|
|
481
|
+
"expression nesting exceeds the maximum depth of #{MAX_NESTING_DEPTH}",
|
|
482
|
+
:nesting_depth_exceeded,
|
|
483
|
+
current.span
|
|
484
|
+
)
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FHIRPath
|
|
4
|
+
# A half-open character range in an expression source string.
|
|
5
|
+
class SourceSpan
|
|
6
|
+
attr_reader :offset, :length
|
|
7
|
+
|
|
8
|
+
def initialize(offset:, length:)
|
|
9
|
+
raise ArgumentError, 'offset must be non-negative' unless offset.is_a?(Integer) && offset >= 0
|
|
10
|
+
raise ArgumentError, 'length must be non-negative' unless length.is_a?(Integer) && length >= 0
|
|
11
|
+
|
|
12
|
+
@offset = offset
|
|
13
|
+
@length = length
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def end_offset
|
|
18
|
+
offset + length
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_h
|
|
22
|
+
{ offset: offset, length: length, end_offset: end_offset }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bigdecimal'
|
|
4
|
+
|
|
5
|
+
module FHIRPath
|
|
6
|
+
# Logical type metadata retained independently of the Ruby runtime class.
|
|
7
|
+
class TypeInfo
|
|
8
|
+
attr_reader :logical_type, :namespace, :model_path, :runtime_class
|
|
9
|
+
|
|
10
|
+
def initialize(logical_type:, namespace: nil, model_path: nil, runtime_class: nil)
|
|
11
|
+
@logical_type = logical_type.to_s.freeze
|
|
12
|
+
@namespace = namespace&.to_s&.freeze
|
|
13
|
+
@model_path = model_path&.to_s&.freeze
|
|
14
|
+
@runtime_class = runtime_class
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_h
|
|
19
|
+
{ logical_type: logical_type, namespace: namespace,
|
|
20
|
+
model_path: model_path, runtime_class: runtime_class }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
module Value
|
|
25
|
+
class Base
|
|
26
|
+
attr_reader :value, :type_info
|
|
27
|
+
|
|
28
|
+
def initialize(value, type_info: nil)
|
|
29
|
+
@value = value
|
|
30
|
+
@type_info = type_info || self.class.default_type_info
|
|
31
|
+
freeze
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.default_type_info
|
|
35
|
+
TypeInfo.new(logical_type: name.split('::').last.downcase)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def to_ruby
|
|
39
|
+
value
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def ==(other)
|
|
43
|
+
other.is_a?(self.class) && other.value == value
|
|
44
|
+
end
|
|
45
|
+
alias eql? ==
|
|
46
|
+
|
|
47
|
+
def hash
|
|
48
|
+
[self.class, value].hash
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class String < Base
|
|
53
|
+
def initialize(value, **kwargs)
|
|
54
|
+
super(value.to_s, **kwargs)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class Integer < Base
|
|
59
|
+
def initialize(value, **kwargs)
|
|
60
|
+
raise ArgumentError, 'FHIRPath integer must be an Integer' unless value.is_a?(::Integer)
|
|
61
|
+
|
|
62
|
+
super(value, **kwargs)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
class Decimal < Base
|
|
67
|
+
def initialize(value, **kwargs)
|
|
68
|
+
decimal = value.is_a?(BigDecimal) ? value : BigDecimal(value.to_s)
|
|
69
|
+
super(decimal, **kwargs)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|