fhirpath-rb 0.1.1
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/.rspec +3 -0
- data/.rubocop.yml +25 -0
- data/CLAUDE.md +59 -0
- data/LICENSE.txt +21 -0
- data/README.md +43 -0
- data/Rakefile +12 -0
- data/ext/fhir_path_parser/extconf.rb +40 -0
- data/lib/fhirpath/engine/evaluators/expressions.rb +59 -0
- data/lib/fhirpath/engine/evaluators/literals.rb +70 -0
- data/lib/fhirpath/engine/evaluators/member_invocation.rb +109 -0
- data/lib/fhirpath/engine/evaluators.rb +130 -0
- data/lib/fhirpath/engine/invocations/aggregate.rb +48 -0
- data/lib/fhirpath/engine/invocations/collections.rb +32 -0
- data/lib/fhirpath/engine/invocations/combining.rb +30 -0
- data/lib/fhirpath/engine/invocations/datetime.rb +34 -0
- data/lib/fhirpath/engine/invocations/equality.rb +95 -0
- data/lib/fhirpath/engine/invocations/equivalence.rb +138 -0
- data/lib/fhirpath/engine/invocations/existence.rb +117 -0
- data/lib/fhirpath/engine/invocations/filtering.rb +107 -0
- data/lib/fhirpath/engine/invocations/inequality.rb +92 -0
- data/lib/fhirpath/engine/invocations/logic.rb +74 -0
- data/lib/fhirpath/engine/invocations/math.rb +109 -0
- data/lib/fhirpath/engine/invocations/math_functions.rb +121 -0
- data/lib/fhirpath/engine/invocations/misc.rb +144 -0
- data/lib/fhirpath/engine/invocations/misc_converts_to.rb +91 -0
- data/lib/fhirpath/engine/invocations/navigation.rb +102 -0
- data/lib/fhirpath/engine/invocations/registry_strings_and_math.rb +48 -0
- data/lib/fhirpath/engine/invocations/strings.rb +139 -0
- data/lib/fhirpath/engine/invocations/strings_encoding.rb +68 -0
- data/lib/fhirpath/engine/invocations/subsetting.rb +15 -0
- data/lib/fhirpath/engine/invocations/types.rb +36 -0
- data/lib/fhirpath/engine/invocations.rb +118 -0
- data/lib/fhirpath/engine/nodes/fp_date_time.rb +140 -0
- data/lib/fhirpath/engine/nodes/fp_date_time_arithmetic.rb +112 -0
- data/lib/fhirpath/engine/nodes/fp_quantity.rb +142 -0
- data/lib/fhirpath/engine/nodes/fp_quantity_deep_equal.rb +67 -0
- data/lib/fhirpath/engine/nodes/fp_time.rb +139 -0
- data/lib/fhirpath/engine/nodes/fp_time_arithmetic.rb +111 -0
- data/lib/fhirpath/engine/nodes/resource_node.rb +66 -0
- data/lib/fhirpath/engine/nodes/type_info.rb +92 -0
- data/lib/fhirpath/engine/param_resolution.rb +74 -0
- data/lib/fhirpath/engine/util.rb +117 -0
- data/lib/fhirpath/engine.rb +100 -0
- data/lib/fhirpath/models/dstu2/choiceTypePaths.json +1041 -0
- data/lib/fhirpath/models/dstu2/path2Type.json +4728 -0
- data/lib/fhirpath/models/dstu2/pathsDefinedElsewhere.json +21 -0
- data/lib/fhirpath/models/dstu2/type2Parent.json +141 -0
- data/lib/fhirpath/models/r4/choiceTypePaths.json +1365 -0
- data/lib/fhirpath/models/r4/path2Type.json +7707 -0
- data/lib/fhirpath/models/r4/pathsDefinedElsewhere.json +57 -0
- data/lib/fhirpath/models/r4/type2Parent.json +211 -0
- data/lib/fhirpath/models/r5/choiceTypePaths.json +1883 -0
- data/lib/fhirpath/models/r5/path2Type.json +9653 -0
- data/lib/fhirpath/models/r5/pathsDefinedElsewhere.json +80 -0
- data/lib/fhirpath/models/r5/type2Parent.json +234 -0
- data/lib/fhirpath/models/stu3/choiceTypePaths.json +991 -0
- data/lib/fhirpath/models/stu3/path2Type.json +5729 -0
- data/lib/fhirpath/models/stu3/pathsDefinedElsewhere.json +37 -0
- data/lib/fhirpath/models/stu3/type2Parent.json +173 -0
- data/lib/fhirpath/models.rb +25 -0
- data/lib/fhirpath/parser/FHIRPath.g4 +172 -0
- data/lib/fhirpath/parser/ast_builder.rb +51 -0
- data/lib/fhirpath/parser.rb +21 -0
- data/lib/fhirpath/version.rb +5 -0
- data/lib/fhirpath.rb +117 -0
- data/rakelib/parser.rake +146 -0
- data/sig/fhirpath.rbs +14 -0
- metadata +168 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
require_relative "math_functions"
|
|
5
|
+
|
|
6
|
+
module Fhirpath
|
|
7
|
+
module Engine
|
|
8
|
+
module Invocations
|
|
9
|
+
# Ruby port of `+`/`-`/`*`/`/`/`div`/`mod`/`&` from fhirpath-py's
|
|
10
|
+
# fhirpathpy/engine/invocations/math.py, including FP_TimeBase/FP_Quantity date
|
|
11
|
+
# arithmetic (`+`/`-` between a date/time and a duration quantity). The single-argument
|
|
12
|
+
# numeric functions (`abs`/`ceiling`/`exp`/`floor`/`ln`/`log`/`power`/`round`/`sqrt`/
|
|
13
|
+
# `truncate`) live in math_functions.rb, reopening this same module.
|
|
14
|
+
module Math
|
|
15
|
+
# Matches Python's decimal module's default context precision (28 significant digits),
|
|
16
|
+
# which is what fhirpath-py's `/` division relies on implicitly — Ruby's BigDecimal `/`
|
|
17
|
+
# has no such fixed precision by default, so without this e.g. `1.2 / 1.8` produces a
|
|
18
|
+
# different (longer, differently-rounded) repeating decimal than the reference.
|
|
19
|
+
DECIMAL_PRECISION = 28
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def plus(_ctx, left, right)
|
|
23
|
+
lhs, rhs = resolve_operands(left, right, "+")
|
|
24
|
+
|
|
25
|
+
return lhs + rhs if (lhs.is_a?(::String) && rhs.is_a?(::String)) ||
|
|
26
|
+
(lhs.is_a?(::Numeric) && rhs.is_a?(::Numeric))
|
|
27
|
+
|
|
28
|
+
date_time_plus(lhs, rhs) { raise Fhirpath::Error, "Cannot #{left} + #{right}" }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def minus(_ctx, left, right)
|
|
32
|
+
lhs, rhs = resolve_operands(left, right, "-")
|
|
33
|
+
|
|
34
|
+
return lhs - rhs if lhs.is_a?(::Numeric) && rhs.is_a?(::Numeric)
|
|
35
|
+
|
|
36
|
+
rhs = Nodes::FPQuantity.new(-rhs.value, rhs.unit) if rhs.is_a?(Nodes::FPQuantity)
|
|
37
|
+
date_time_plus(lhs, rhs) { raise Fhirpath::Error, "Cannot #{left} - #{right}" }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def mul(_ctx, lhs, rhs)
|
|
41
|
+
lhs * rhs
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def div(_ctx, lhs, rhs)
|
|
45
|
+
return [] if rhs.zero?
|
|
46
|
+
|
|
47
|
+
Util.to_big_decimal(lhs).div(Util.to_big_decimal(rhs), DECIMAL_PRECISION)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def intdiv(_ctx, lhs, rhs)
|
|
51
|
+
return [] if rhs.zero?
|
|
52
|
+
|
|
53
|
+
Util.to_big_decimal(lhs).div(Util.to_big_decimal(rhs), DECIMAL_PRECISION).truncate
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def mod(_ctx, lhs, rhs)
|
|
57
|
+
return [] if rhs.zero?
|
|
58
|
+
|
|
59
|
+
lhs % rhs
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def amp(_ctx, lhs, rhs)
|
|
63
|
+
lhs = "" if lhs == []
|
|
64
|
+
rhs = "" if rhs == []
|
|
65
|
+
lhs + rhs
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Mirrors fhirpath-py's own `xs = remove_duplicate_extension(xs_)` at the top of
|
|
71
|
+
# `plus`/`minus`: strip the "Cannot ... " length check down to the primitive operand,
|
|
72
|
+
# then unwrap it from its ResourceNode.
|
|
73
|
+
def resolve_operands(left, right, operator)
|
|
74
|
+
left = Util.remove_duplicate_extension(left)
|
|
75
|
+
right = Util.remove_duplicate_extension(right)
|
|
76
|
+
raise Fhirpath::Error, "Cannot #{left} #{operator} #{right}" if left.length != 1 || right.length != 1
|
|
77
|
+
|
|
78
|
+
[Util.get_data(left[0]), Util.get_data(right[0])]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# A duration quantity added to (or, via `minus`, subtracted from) a date/time value —
|
|
82
|
+
# https://hl7.org/fhirpath/#datetime-arithmetic. `lhs` may already be an FPDateTime/
|
|
83
|
+
# FPTime (e.g. the left side was itself a date/time literal), or a raw String that
|
|
84
|
+
# happens to be date/time-shaped (e.g. a FHIR "date"/"dateTime"/"time" element's raw
|
|
85
|
+
# value navigated without a model, so it was never converted to an FP_* type) — either
|
|
86
|
+
# way, if it parses as a date/time and `rhs` is a quantity, delegate to its `#plus`.
|
|
87
|
+
def date_time_plus(lhs, rhs)
|
|
88
|
+
return yield unless rhs.is_a?(Nodes::FPQuantity)
|
|
89
|
+
|
|
90
|
+
target = lhs.is_a?(::String) ? coerce_date_or_time(lhs) : lhs
|
|
91
|
+
return yield unless target.is_a?(Nodes::FPDateTime) || target.is_a?(Nodes::FPTime)
|
|
92
|
+
|
|
93
|
+
target.plus(rhs)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def coerce_date_or_time(str)
|
|
97
|
+
Nodes::FPDateTime.new(str)
|
|
98
|
+
rescue Fhirpath::Error
|
|
99
|
+
begin
|
|
100
|
+
Nodes::FPTime.new(str)
|
|
101
|
+
rescue Fhirpath::Error
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
require "bigdecimal/math"
|
|
5
|
+
|
|
6
|
+
module Fhirpath
|
|
7
|
+
module Engine
|
|
8
|
+
module Invocations
|
|
9
|
+
# Ruby port of `abs`/`ceiling`/`exp`/`floor`/`ln`/`log`/`power`/`round`/`sqrt`/`truncate`
|
|
10
|
+
# from fhirpath-py's fhirpathpy/engine/invocations/math.py — the single-argument (plus
|
|
11
|
+
# `log`/`power`/`round`'s second argument) numeric functions. Reopens the Math module
|
|
12
|
+
# defined in math.rb, which holds the `+`/`-`/`*`/`/`/`div`/`mod`/`&` infix operators.
|
|
13
|
+
module Math
|
|
14
|
+
LOG_PRECISION = 30
|
|
15
|
+
LOG_SCALE = 15
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def abs(_ctx, num)
|
|
19
|
+
return [] if blank?(num)
|
|
20
|
+
|
|
21
|
+
ensure_number_singleton(num).abs
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def ceiling(_ctx, num)
|
|
25
|
+
return [] if blank?(num)
|
|
26
|
+
|
|
27
|
+
ensure_number_singleton(num).ceil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def floor(_ctx, num)
|
|
31
|
+
return [] if blank?(num)
|
|
32
|
+
|
|
33
|
+
ensure_number_singleton(num).floor
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def truncate(_ctx, num)
|
|
37
|
+
return [] if blank?(num)
|
|
38
|
+
|
|
39
|
+
ensure_number_singleton(num).truncate
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def exp(_ctx, num)
|
|
43
|
+
return [] if blank?(num)
|
|
44
|
+
|
|
45
|
+
BigMath.exp(Util.to_big_decimal(ensure_number_singleton(num)), LOG_PRECISION)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def ln(_ctx, num)
|
|
49
|
+
return [] if blank?(num)
|
|
50
|
+
|
|
51
|
+
BigMath.log(Util.to_big_decimal(ensure_number_singleton(num)), LOG_PRECISION)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def log(_ctx, num, base)
|
|
55
|
+
return [] if blank?(num) || blank?(base)
|
|
56
|
+
|
|
57
|
+
value = Util.to_big_decimal(ensure_number_singleton(num))
|
|
58
|
+
base_value = Util.to_big_decimal(ensure_number_singleton(base))
|
|
59
|
+
|
|
60
|
+
(BigMath.log(value, LOG_PRECISION) / BigMath.log(base_value, LOG_PRECISION)).round(LOG_SCALE)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def sqrt(_ctx, num)
|
|
64
|
+
return [] if blank?(num)
|
|
65
|
+
|
|
66
|
+
value = ensure_number_singleton(num)
|
|
67
|
+
return [] if value.negative?
|
|
68
|
+
|
|
69
|
+
Util.to_big_decimal(value).sqrt(LOG_PRECISION)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def power(_ctx, num, degree)
|
|
73
|
+
return [] if blank?(num) || blank?(degree)
|
|
74
|
+
|
|
75
|
+
base = ensure_number_singleton(num)
|
|
76
|
+
exponent = ensure_number_singleton(degree)
|
|
77
|
+
return [] if base.negative? || exponent.to_i != exponent
|
|
78
|
+
|
|
79
|
+
base**exponent
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def round(_ctx, num, precision)
|
|
83
|
+
return [] if blank?(num)
|
|
84
|
+
|
|
85
|
+
value = ensure_number_singleton(num)
|
|
86
|
+
return value.round if blank?(precision)
|
|
87
|
+
|
|
88
|
+
value.round(ensure_number_singleton(precision).to_i)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
# Mirrors fhirpath-py's is_empty: the argument may be the raw input collection
|
|
94
|
+
# (always an array) or an already-resolved param value (a raw number, or `[]` if the
|
|
95
|
+
# param expression itself was empty).
|
|
96
|
+
def blank?(value)
|
|
97
|
+
return false if value.is_a?(::Numeric)
|
|
98
|
+
|
|
99
|
+
Util.empty?(value)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Mirrors fhirpath-py's ensure_number_singleton: accepts either a raw number
|
|
103
|
+
# (an already-resolved param) or a singleton collection (the raw input data).
|
|
104
|
+
def ensure_number_singleton(value)
|
|
105
|
+
data = Util.get_data(value)
|
|
106
|
+
return data if data.is_a?(::Numeric)
|
|
107
|
+
|
|
108
|
+
unless data.is_a?(::Array) && data.length == 1
|
|
109
|
+
raise Fhirpath::Error, "Expected list with number, but got #{data.inspect}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
item = Util.get_data(data.first)
|
|
113
|
+
raise Fhirpath::Error, "Expected number, but got #{value.inspect}" unless item.is_a?(::Numeric)
|
|
114
|
+
|
|
115
|
+
item
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
|
|
5
|
+
module Fhirpath
|
|
6
|
+
module Engine
|
|
7
|
+
module Invocations
|
|
8
|
+
# Ruby port of `iif`/`trace`/`toInteger`/`toDecimal`/`toString`/`toDate`/`toDateTime`/
|
|
9
|
+
# `toTime`/`toQuantity` from fhirpath-py's fhirpathpy/engine/invocations/misc.py.
|
|
10
|
+
# `toBoolean`/the convertsTo* family live in misc_converts_to.rb, reopening this module.
|
|
11
|
+
module Misc
|
|
12
|
+
INT_REGEX = /\A[+-]?\d+\z/
|
|
13
|
+
NUM_REGEX = /\A[+-]?\d+(\.\d+)?\z/
|
|
14
|
+
QUANTITY_REGEX = /\A([+-]?\d+(?:\.\d+)?)\s*(?:('[^']+')|([a-zA-Z]+))?\z/
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def iif(_ctx, data, cond, on_true, on_false = nil)
|
|
18
|
+
return on_true.call(data) if Util.true?(cond.call(data))
|
|
19
|
+
return on_false.call(data) if on_false
|
|
20
|
+
|
|
21
|
+
[]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Logs the input collection under `label` (via ctx[:trace_fn] if the caller supplied
|
|
25
|
+
# one, else stdout) and returns it unchanged. Mirrors fhirpath-py: a second
|
|
26
|
+
# ("projection") argument is accepted but not used for anything — fhirpath-py itself
|
|
27
|
+
# silently drops it (engine/__init__.py truncates trace's raw_params to the first
|
|
28
|
+
# element before arity resolution) rather than actually implementing the spec's
|
|
29
|
+
# "trace with a projected value" behavior.
|
|
30
|
+
def trace(ctx, coll, label = "", _projection = nil)
|
|
31
|
+
if ctx[:trace_fn].respond_to?(:call)
|
|
32
|
+
ctx[:trace_fn].call(label, coll)
|
|
33
|
+
else
|
|
34
|
+
puts "TRACE:[#{label}] #{coll}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
coll
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_integer(_ctx, coll)
|
|
41
|
+
return [] if coll.length != 1
|
|
42
|
+
|
|
43
|
+
integer_value(Util.get_data(coll.first))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def to_decimal(_ctx, coll)
|
|
47
|
+
return [] if coll.length != 1
|
|
48
|
+
|
|
49
|
+
value = Util.get_data(coll.first)
|
|
50
|
+
return BigDecimal(0) if value == false
|
|
51
|
+
return BigDecimal(1) if value == true
|
|
52
|
+
return BigDecimal(value.to_s) if value.is_a?(::Numeric)
|
|
53
|
+
return BigDecimal(value) if value.is_a?(::String) && NUM_REGEX.match?(value)
|
|
54
|
+
|
|
55
|
+
[]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def to_string(_ctx, coll)
|
|
59
|
+
return [] if coll.length != 1
|
|
60
|
+
|
|
61
|
+
Util.format_number(Util.get_data(coll.first))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def to_date_time(_ctx, coll)
|
|
65
|
+
raise Fhirpath::Error, "to_date_time called for a collection of length #{coll.length}" if coll.length > 1
|
|
66
|
+
return [] if coll.empty?
|
|
67
|
+
|
|
68
|
+
Nodes::FPDateTime.new(Util.get_data(coll.first))
|
|
69
|
+
rescue Fhirpath::Error
|
|
70
|
+
[]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# fhirpath-py's to_date is, as written, identical to to_date_time (no truncation to
|
|
74
|
+
# date-only precision) — mirrored as-is.
|
|
75
|
+
def to_date(ctx, coll)
|
|
76
|
+
to_date_time(ctx, coll)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def to_time(_ctx, coll)
|
|
80
|
+
raise Fhirpath::Error, "to_time called for a collection of length #{coll.length}" if coll.length > 1
|
|
81
|
+
return [] if coll.empty?
|
|
82
|
+
|
|
83
|
+
Nodes::FPTime.new(Util.get_data(coll.first))
|
|
84
|
+
rescue Fhirpath::Error
|
|
85
|
+
[]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def to_quantity(_ctx, coll, to_unit = nil)
|
|
89
|
+
if coll.length > 1
|
|
90
|
+
raise Fhirpath::Error, "Could not convert to quantity: input collection contains multiple items"
|
|
91
|
+
end
|
|
92
|
+
return [] if coll.empty?
|
|
93
|
+
|
|
94
|
+
to_unit = normalize_unit(to_unit) if to_unit
|
|
95
|
+
result = parse_quantity(Util.val_data_converted(coll.first))
|
|
96
|
+
result = convert_to_unit(result, to_unit) if result && to_unit
|
|
97
|
+
|
|
98
|
+
result || []
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def integer_value(value)
|
|
104
|
+
return 0 if value == false
|
|
105
|
+
return 1 if value == true
|
|
106
|
+
return value if value.is_a?(::Numeric) && value.to_i == value
|
|
107
|
+
return value.to_i if value.is_a?(::String) && INT_REGEX.match?(value)
|
|
108
|
+
|
|
109
|
+
[]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def normalize_unit(unit)
|
|
113
|
+
Nodes::FPQuantity::TIME_UNITS_TO_UCUM.key?(unit) ? unit : "'#{unit}'"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def convert_to_unit(result, to_unit)
|
|
117
|
+
return result if result.unit == to_unit
|
|
118
|
+
|
|
119
|
+
Nodes::FPQuantity.conv_unit_to(result.unit, result.value, to_unit)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def parse_quantity(value)
|
|
123
|
+
return Nodes::FPQuantity.new(value, "'1'") if value.is_a?(::Numeric)
|
|
124
|
+
return value if value.is_a?(Nodes::FPQuantity)
|
|
125
|
+
return Nodes::FPQuantity.new(value ? 1 : 0, "'1'") if [true, false].include?(value)
|
|
126
|
+
return parse_quantity_string(value) if value.is_a?(::String)
|
|
127
|
+
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def parse_quantity_string(value)
|
|
132
|
+
match = QUANTITY_REGEX.match(value)
|
|
133
|
+
return nil unless match
|
|
134
|
+
|
|
135
|
+
time = match[3]
|
|
136
|
+
return nil if time && !Nodes::FPQuantity::TIME_UNITS_TO_UCUM.key?(time)
|
|
137
|
+
|
|
138
|
+
Nodes::FPQuantity.new(BigDecimal(match[1]), match[2] || time || "'1'")
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
|
|
5
|
+
module Fhirpath
|
|
6
|
+
module Engine
|
|
7
|
+
module Invocations
|
|
8
|
+
# Reopens Misc (see misc.rb) to add `toBoolean`/the convertsTo* family, ported from
|
|
9
|
+
# fhirpath-py's fhirpathpy/engine/invocations/misc.py.
|
|
10
|
+
module Misc
|
|
11
|
+
class << self
|
|
12
|
+
TRUE_STRINGS = %w[true t yes y 1 1.0].freeze
|
|
13
|
+
FALSE_STRINGS = %w[false f no n 0 0.0].freeze
|
|
14
|
+
|
|
15
|
+
# Unlike the other to_* conversions, this checks the raw collection item's own type
|
|
16
|
+
# directly (not Util.get_data-unwrapped) — mirrors fhirpath-py's to_boolean.
|
|
17
|
+
def to_boolean(_ctx, coll)
|
|
18
|
+
return [] if coll.length != 1
|
|
19
|
+
|
|
20
|
+
value = coll.first
|
|
21
|
+
return value if [true, false].include?(value)
|
|
22
|
+
return boolean_from_number(value) if value.is_a?(::Numeric)
|
|
23
|
+
return boolean_from_string(value) if value.is_a?(::String)
|
|
24
|
+
|
|
25
|
+
[]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Ports fhirpath-py's create_converts_to_fn: the collection must be a singleton, and
|
|
29
|
+
# the conversion must both succeed (not fall back to `[]`) and produce a value of the
|
|
30
|
+
# expected Ruby class.
|
|
31
|
+
def converts_to?(ctx, coll, expected_class, &converter)
|
|
32
|
+
return [] if coll.length != 1
|
|
33
|
+
|
|
34
|
+
converter.call(ctx, coll).is_a?(expected_class)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def converts_to_boolean(ctx, coll)
|
|
38
|
+
return [] if coll.length != 1
|
|
39
|
+
|
|
40
|
+
[true, false].include?(to_boolean(ctx, coll))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def converts_to_integer(ctx, coll)
|
|
44
|
+
converts_to?(ctx, coll, ::Integer) { |c, co| to_integer(c, co) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def converts_to_decimal(ctx, coll)
|
|
48
|
+
converts_to?(ctx, coll, ::BigDecimal) { |c, co| to_decimal(c, co) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def converts_to_string(ctx, coll)
|
|
52
|
+
converts_to?(ctx, coll, ::String) { |c, co| to_string(c, co) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def converts_to_date(ctx, coll)
|
|
56
|
+
converts_to?(ctx, coll, Nodes::FPDateTime) { |c, co| to_date(c, co) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def converts_to_date_time(ctx, coll)
|
|
60
|
+
converts_to?(ctx, coll, Nodes::FPDateTime) { |c, co| to_date_time(c, co) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def converts_to_time(ctx, coll)
|
|
64
|
+
converts_to?(ctx, coll, Nodes::FPTime) { |c, co| to_time(c, co) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def converts_to_quantity(ctx, coll)
|
|
68
|
+
converts_to?(ctx, coll, Nodes::FPQuantity) { |c, co| to_quantity(c, co) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def boolean_from_number(value)
|
|
74
|
+
return true if [1, 1.0].include?(value)
|
|
75
|
+
return false if [0, 0.0].include?(value)
|
|
76
|
+
|
|
77
|
+
[]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def boolean_from_string(value)
|
|
81
|
+
lower = value.downcase
|
|
82
|
+
return true if TRUE_STRINGS.include?(lower)
|
|
83
|
+
return false if FALSE_STRINGS.include?(lower)
|
|
84
|
+
|
|
85
|
+
[]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fhirpath
|
|
4
|
+
module Engine
|
|
5
|
+
module Invocations
|
|
6
|
+
# Ruby port of `children`/`descendants` from fhirpath-py's
|
|
7
|
+
# fhirpathpy/engine/invocations/navigation.py.
|
|
8
|
+
module Navigation
|
|
9
|
+
class << self
|
|
10
|
+
def children(ctx, coll)
|
|
11
|
+
coll.reduce([]) { |acc, res| reduce_children(ctx, acc, res, exclude_primitive_extensions: true) }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def descendants(ctx, coll)
|
|
15
|
+
res = []
|
|
16
|
+
current = reduce_all_children(ctx, coll)
|
|
17
|
+
|
|
18
|
+
until current.empty?
|
|
19
|
+
res += current
|
|
20
|
+
current = reduce_all_children(ctx, current)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
res
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def reduce_all_children(ctx, coll)
|
|
29
|
+
coll.reduce([]) { |acc, item| reduce_children(ctx, acc, item, exclude_primitive_extensions: false) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def reduce_children(ctx, acc, res, exclude_primitive_extensions:)
|
|
33
|
+
model = ctx[:model]
|
|
34
|
+
data = Util.get_data(res)
|
|
35
|
+
res = Nodes::ResourceNode.create_node(res)
|
|
36
|
+
|
|
37
|
+
data = data.each_with_index.to_h { |value, i| [i, value] } if data.is_a?(::Array)
|
|
38
|
+
return acc unless data.is_a?(::Hash)
|
|
39
|
+
|
|
40
|
+
data.each_key do |prop|
|
|
41
|
+
next if prop.start_with?("_") && exclude_primitive_extensions
|
|
42
|
+
|
|
43
|
+
acc = reduce_child(model, acc, res, prop, data[prop])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
acc
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def reduce_child(model, acc, res, prop, value)
|
|
50
|
+
child_path, full_path = child_paths(model, res, prop)
|
|
51
|
+
|
|
52
|
+
if value.is_a?(::Array)
|
|
53
|
+
value.each_with_index do |item, i|
|
|
54
|
+
acc += [Nodes::ResourceNode.create_node(item, child_path, prop_name: "#{full_path}[#{i}]", index: i)]
|
|
55
|
+
end
|
|
56
|
+
else
|
|
57
|
+
acc += [Nodes::ResourceNode.create_node(value, child_path, prop_name: full_path)]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
acc
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def child_paths(model, res, prop)
|
|
64
|
+
child_path = res.path ? "#{res.path}.#{prop}" : ""
|
|
65
|
+
full_path = (res.prop_name ? "#{res.prop_name}.#{prop}" : child_path).delete("_")
|
|
66
|
+
|
|
67
|
+
child_path = "Extension" if prop == "extension"
|
|
68
|
+
child_path = remap(model, "pathsDefinedElsewhere", child_path)
|
|
69
|
+
child_path = remap(model, "path2Type", child_path)
|
|
70
|
+
full_path = choice_type_full_path(model, res, prop, child_path) || full_path
|
|
71
|
+
|
|
72
|
+
[child_path, full_path]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def remap(model, table, child_path)
|
|
76
|
+
return child_path unless model.is_a?(::Hash) && model[table]
|
|
77
|
+
|
|
78
|
+
model[table].fetch(child_path, child_path)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# When a property's name already carries its (shrunk-to-a-bare-type) childPath as a
|
|
82
|
+
# suffix (e.g. prop "valueString" vs childPath "string"), it's really a choice-type
|
|
83
|
+
# field — fullPath should point at the shared field name ("value"), not "valueString".
|
|
84
|
+
def choice_type_full_path(model, res, prop, child_path)
|
|
85
|
+
return nil unless prop.downcase.end_with?(child_path.downcase) && prop.length > child_path.length
|
|
86
|
+
|
|
87
|
+
base_prop = prop[0...-child_path.length]
|
|
88
|
+
return nil if choice_types(model, "#{res.path}.#{base_prop}").empty?
|
|
89
|
+
|
|
90
|
+
"#{res.prop_name}.#{base_prop}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def choice_types(model, alt_prop_name)
|
|
94
|
+
return [] unless model.is_a?(::Hash)
|
|
95
|
+
|
|
96
|
+
(model["choiceTypePaths"] || {})[alt_prop_name] || []
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fhirpath
|
|
4
|
+
module Engine
|
|
5
|
+
module Invocations
|
|
6
|
+
# The string functions and single-argument math functions half of REGISTRY (see
|
|
7
|
+
# invocations.rb, which merges this in) — split out purely to keep that module's line
|
|
8
|
+
# count down.
|
|
9
|
+
REGISTRY_STRINGS_AND_MATH = {
|
|
10
|
+
"indexOf" => { fn: Strings.method(:index_of), arity: { 1 => ["String"] }, nullable_input: true },
|
|
11
|
+
"substring" => {
|
|
12
|
+
fn: Strings.method(:substring),
|
|
13
|
+
arity: { 1 => ["Integer"], 2 => %w[Integer Integer] },
|
|
14
|
+
nullable_input: true
|
|
15
|
+
},
|
|
16
|
+
"startsWith" => { fn: Strings.method(:starts_with), arity: { 1 => ["String"] }, nullable_input: true },
|
|
17
|
+
"endsWith" => { fn: Strings.method(:ends_with), arity: { 1 => ["String"] }, nullable_input: true },
|
|
18
|
+
"contains" => { fn: Strings.method(:contains), arity: { 1 => ["String"] }, nullable_input: true },
|
|
19
|
+
"upper" => { fn: Strings.method(:upper), nullable_input: true },
|
|
20
|
+
"lower" => { fn: Strings.method(:lower), nullable_input: true },
|
|
21
|
+
"replace" => { fn: Strings.method(:replace), arity: { 2 => %w[String String] }, nullable_input: true },
|
|
22
|
+
"matches" => { fn: Strings.method(:matches), arity: { 1 => ["String"] }, nullable_input: true },
|
|
23
|
+
"replaceMatches" => {
|
|
24
|
+
fn: Strings.method(:replace_matches),
|
|
25
|
+
arity: { 2 => %w[String String] },
|
|
26
|
+
nullable_input: true
|
|
27
|
+
},
|
|
28
|
+
"length" => { fn: Strings.method(:length), nullable_input: true },
|
|
29
|
+
"toChars" => { fn: Strings.method(:to_chars) },
|
|
30
|
+
"join" => { fn: Strings.method(:join), arity: { 0 => [], 1 => ["String"] } },
|
|
31
|
+
"split" => { fn: Strings.method(:split), arity: { 1 => ["String"] }, nullable_input: true },
|
|
32
|
+
"trim" => { fn: Strings.method(:trim), nullable_input: true },
|
|
33
|
+
"encode" => { fn: Strings.method(:encode), arity: { 1 => ["String"] } },
|
|
34
|
+
"decode" => { fn: Strings.method(:decode), arity: { 1 => ["String"] } },
|
|
35
|
+
"abs" => { fn: Math.method(:abs) },
|
|
36
|
+
"ceiling" => { fn: Math.method(:ceiling) },
|
|
37
|
+
"exp" => { fn: Math.method(:exp) },
|
|
38
|
+
"floor" => { fn: Math.method(:floor) },
|
|
39
|
+
"ln" => { fn: Math.method(:ln) },
|
|
40
|
+
"log" => { fn: Math.method(:log), arity: { 1 => ["Number"] }, nullable: true },
|
|
41
|
+
"power" => { fn: Math.method(:power), arity: { 1 => ["Number"] }, nullable: true },
|
|
42
|
+
"round" => { fn: Math.method(:round), arity: { 1 => ["Number"] } },
|
|
43
|
+
"sqrt" => { fn: Math.method(:sqrt) },
|
|
44
|
+
"truncate" => { fn: Math.method(:truncate) }
|
|
45
|
+
}.freeze
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|