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,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fhirpath
|
|
4
|
+
# Resolves a raw AST param node into the value/lambda an invocation function expects, given
|
|
5
|
+
# its declared param type ("Expr", "Any", "AnyAtRoot", "TypeSpecifier", "Integer", "String").
|
|
6
|
+
# Ported from fhirpath-py's fhirpathpy/engine/__init__.py make_param + type_specifier. Split
|
|
7
|
+
# out of engine.rb to keep that file's module body short; reopens the same Engine module.
|
|
8
|
+
module Engine
|
|
9
|
+
class << self
|
|
10
|
+
SINGLETON_PARAM_CHECKS = {
|
|
11
|
+
"Integer" => ->(data) { data.is_a?(::Numeric) && data.to_i == data },
|
|
12
|
+
"Number" => ->(data) { data.is_a?(::Numeric) },
|
|
13
|
+
"String" => ->(data) { data.is_a?(::String) },
|
|
14
|
+
"Boolean" => ->(data) { [true, false].include?(data) }
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def make_param(ctx, parent_data, node_type, param)
|
|
18
|
+
return build_expr_param(ctx, param) if node_type == "Expr"
|
|
19
|
+
return eval_param(ctx, parent_data, param) if node_type == "Any"
|
|
20
|
+
return eval_param(ctx, ctx[:this] || ctx[:root], param) if node_type == "AnyAtRoot"
|
|
21
|
+
return type_specifier(param) if node_type == "TypeSpecifier"
|
|
22
|
+
return check_singleton_param(ctx, parent_data, param, node_type) if SINGLETON_PARAM_CHECKS.key?(node_type)
|
|
23
|
+
|
|
24
|
+
raise Fhirpath::Error, "Implement me for #{node_type}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
# Ports fhirpath-py's make_param's singleton + param_check_table handling: evaluate the
|
|
30
|
+
# param as a singleton against parent_data, then require it to satisfy the given type.
|
|
31
|
+
def check_singleton_param(ctx, parent_data, param, type_name)
|
|
32
|
+
ctx[:this] = parent_data
|
|
33
|
+
res = do_eval(ctx, parent_data, param)
|
|
34
|
+
return [] if res.empty?
|
|
35
|
+
|
|
36
|
+
raise Fhirpath::Error, "Unexpected collection; expected singleton of type #{type_name}" if res.length > 1
|
|
37
|
+
|
|
38
|
+
data = Util.get_data(res.first)
|
|
39
|
+
unless SINGLETON_PARAM_CHECKS[type_name].call(data)
|
|
40
|
+
raise Fhirpath::Error, "Expected #{type_name.downcase}, got: #{data.inspect}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
data
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Ports fhirpath-py's module-level type_specifier: regardless of how the parameter node
|
|
47
|
+
# is shaped internally, its source text (e.g. "string", "FHIR.Patient") is what matters.
|
|
48
|
+
def type_specifier(node)
|
|
49
|
+
identifiers = node["text"].delete("`").split(".")
|
|
50
|
+
|
|
51
|
+
case identifiers.length
|
|
52
|
+
when 1
|
|
53
|
+
Nodes::TypeInfo.new(identifiers.first, nil)
|
|
54
|
+
when 2
|
|
55
|
+
Nodes::TypeInfo.new(identifiers[1], identifiers[0])
|
|
56
|
+
else
|
|
57
|
+
raise Fhirpath::Error, "Expected TypeSpecifier node, got #{node}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def build_expr_param(ctx, param)
|
|
62
|
+
lambda do |data|
|
|
63
|
+
ctx[:this] = Util.arraify(data)
|
|
64
|
+
do_eval(ctx, ctx[:this], param)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def eval_param(ctx, data, param)
|
|
69
|
+
ctx[:this] = data
|
|
70
|
+
do_eval(ctx, data, param)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
|
|
5
|
+
module Fhirpath
|
|
6
|
+
module Engine
|
|
7
|
+
# Ruby port of fhirpath-py's fhirpathpy/engine/util.py: small helpers shared across the
|
|
8
|
+
# evaluator, invocation dispatch, and invocation implementations.
|
|
9
|
+
module Util
|
|
10
|
+
class << self
|
|
11
|
+
def get_data(value)
|
|
12
|
+
value.is_a?(Nodes::ResourceNode) ? value.data : value
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def val_data_converted(value)
|
|
16
|
+
value.is_a?(Nodes::ResourceNode) ? value.convert_data : value
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def capitalized?(value)
|
|
20
|
+
value.is_a?(::String) && !value.empty? && value[0] == value[0].upcase
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def empty?(value)
|
|
24
|
+
value.is_a?(::Array) && value.empty?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def some?(value)
|
|
28
|
+
!value.nil? && !empty?(value)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def nullable?(value)
|
|
32
|
+
value.nil? || empty?(value)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def arraify(value, instead_none = nil)
|
|
36
|
+
return value if value.is_a?(::Array)
|
|
37
|
+
return [value] if some?(value)
|
|
38
|
+
|
|
39
|
+
instead_none.nil? ? [] : [instead_none]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def flatten(values)
|
|
43
|
+
values.each_with_object([]) do |value, acc|
|
|
44
|
+
value.is_a?(::Array) ? acc.concat(value) : acc << value
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def true?(value)
|
|
49
|
+
value == true || (value.is_a?(::Array) && value.length == 1 && value.first == true)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# BigDecimal#to_s always includes a fractional part (e.g. "7.0"); FHIRPath's decimal
|
|
53
|
+
# string representation (used by `toString`, FP_Quantity#to_s, ...) drops it for
|
|
54
|
+
# whole-number values (e.g. "7"), matching Python's Decimal str().
|
|
55
|
+
def format_number(value)
|
|
56
|
+
return value.to_s unless value.is_a?(::BigDecimal)
|
|
57
|
+
|
|
58
|
+
value.frac.zero? ? value.to_i.to_s : value.to_s("F")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def to_big_decimal(value)
|
|
62
|
+
return value if value.is_a?(::BigDecimal)
|
|
63
|
+
return BigDecimal(value) if value.is_a?(::Integer)
|
|
64
|
+
|
|
65
|
+
BigDecimal(value.to_s)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Dedupes values that compare equal after normalizing hash key order (so
|
|
69
|
+
# `{"a"=>1,"b"=>2}` and `{"b"=>2,"a"=>1}` collapse together), preserving first-seen
|
|
70
|
+
# order — mirrors fhirpath-py's util.uniq (JSON-with-sorted-keys as the dedup key).
|
|
71
|
+
# A FHIR primitive element with an "_x" extension companion (e.g. "birthDate" +
|
|
72
|
+
# "_birthDate") navigates to a 2-item collection: the primitive value, then a
|
|
73
|
+
# ResourceNode wrapping just its `{"extension" => [...]}` sibling. Mirrors fhirpath-py's
|
|
74
|
+
# equality.remove_duplicate_extension (its own comment calls this "a temporary
|
|
75
|
+
# solution... needs to be fixed to a better solution") — used wherever only the
|
|
76
|
+
# primitive value matters (`+`/`-`, singleton type/length checks for `<`/`>`/`<=`/`>=`).
|
|
77
|
+
def remove_duplicate_extension(list)
|
|
78
|
+
second = list[1]
|
|
79
|
+
return list unless list.length == 2 && second.is_a?(Nodes::ResourceNode) &&
|
|
80
|
+
second.data.is_a?(::Hash) && second.data.key?("extension")
|
|
81
|
+
|
|
82
|
+
list.first(1)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def uniq(values)
|
|
86
|
+
seen = {}
|
|
87
|
+
values.each_with_object([]) do |value, acc|
|
|
88
|
+
key = canonical_key(value)
|
|
89
|
+
next if seen[key]
|
|
90
|
+
|
|
91
|
+
seen[key] = true
|
|
92
|
+
acc << value
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def canonical_key(value)
|
|
99
|
+
sorted_keys(value).to_s
|
|
100
|
+
rescue ::StandardError
|
|
101
|
+
value.to_s
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def sorted_keys(value)
|
|
105
|
+
case value
|
|
106
|
+
when ::Hash
|
|
107
|
+
value.keys.sort_by(&:to_s).each_with_object({}) { |k, h| h[k] = sorted_keys(value[k]) }
|
|
108
|
+
when ::Array
|
|
109
|
+
value.map { |v| sorted_keys(v) }
|
|
110
|
+
else
|
|
111
|
+
value
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "engine/nodes/resource_node"
|
|
4
|
+
require_relative "engine/nodes/fp_quantity"
|
|
5
|
+
require_relative "engine/nodes/fp_quantity_deep_equal"
|
|
6
|
+
require_relative "engine/nodes/fp_date_time"
|
|
7
|
+
require_relative "engine/nodes/fp_date_time_arithmetic"
|
|
8
|
+
require_relative "engine/nodes/fp_time"
|
|
9
|
+
require_relative "engine/nodes/fp_time_arithmetic"
|
|
10
|
+
require_relative "engine/nodes/type_info"
|
|
11
|
+
require_relative "engine/util"
|
|
12
|
+
require_relative "engine/invocations"
|
|
13
|
+
require_relative "engine/evaluators"
|
|
14
|
+
require_relative "engine/param_resolution"
|
|
15
|
+
|
|
16
|
+
module Fhirpath
|
|
17
|
+
# Ruby port of fhirpath-py's fhirpathpy/engine/__init__.py: walks the AST built by
|
|
18
|
+
# Fhirpath::Parser (see Evaluators::DISPATCH for the per-node-type evaluators) and dispatches
|
|
19
|
+
# function/operator calls to Invocations::REGISTRY.
|
|
20
|
+
module Engine
|
|
21
|
+
class << self
|
|
22
|
+
def do_eval(ctx, parent_data, node)
|
|
23
|
+
evaluator = Evaluators::DISPATCH[node["type"]]
|
|
24
|
+
raise Fhirpath::Error, "No #{node["type"]} evaluator" unless evaluator
|
|
25
|
+
|
|
26
|
+
evaluator.call(ctx, parent_data, node)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def do_invoke(ctx, fn_name, data, raw_params)
|
|
30
|
+
fn_name, invocation = lookup_invocation(ctx, fn_name)
|
|
31
|
+
return [] if invocation[:nullable_input] && Util.nullable?(data)
|
|
32
|
+
return invoke_variadic(ctx, invocation, data, raw_params) if invocation[:variadic]
|
|
33
|
+
return call_niladic(ctx, fn_name, invocation, data, raw_params) unless invocation[:arity]
|
|
34
|
+
|
|
35
|
+
invoke_with_arity(ctx, fn_name, invocation, data, raw_params)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def infix_invoke(ctx, fn_name, data, raw_params)
|
|
39
|
+
invocation = invocation_registry(ctx)[fn_name]
|
|
40
|
+
raise Fhirpath::Error, "Not implemented #{fn_name}" unless invocation&.fetch(:fn, nil)
|
|
41
|
+
raise Fhirpath::Error, "Infix invoke should have arity 2" if raw_params.length != 2
|
|
42
|
+
|
|
43
|
+
params = [ctx] + positional_params(ctx, data, invocation[:arity][2], raw_params, 2)
|
|
44
|
+
call_invocation(invocation, params)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def lookup_invocation(ctx, fn_name)
|
|
50
|
+
registry = invocation_registry(ctx)
|
|
51
|
+
fn_name = fn_name.first if fn_name.is_a?(::Array) && fn_name.length == 1
|
|
52
|
+
raise Fhirpath::Error, "Not implemented: #{fn_name}" unless fn_name.is_a?(::String) && registry.key?(fn_name)
|
|
53
|
+
|
|
54
|
+
[fn_name, registry[fn_name]]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def call_niladic(ctx, fn_name, invocation, data, raw_params)
|
|
58
|
+
raise Fhirpath::Error, "#{fn_name} expects no params" unless raw_params.nil? || Util.empty?(raw_params)
|
|
59
|
+
|
|
60
|
+
Util.arraify(invocation[:fn].call(ctx, Util.arraify(data)))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def invoke_variadic(ctx, invocation, data, raw_params)
|
|
64
|
+
raw_params_list = raw_params.is_a?(::Array) ? raw_params : []
|
|
65
|
+
this_value = ctx[:this] || ctx[:root]
|
|
66
|
+
params = raw_params_list.map { |param| make_param(ctx, this_value, invocation[:variadic], param) }
|
|
67
|
+
|
|
68
|
+
Util.arraify(invocation[:fn].call(ctx, Util.arraify(data), *params))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def invoke_with_arity(ctx, fn_name, invocation, data, raw_params)
|
|
72
|
+
params_number = raw_params.is_a?(::Array) ? raw_params.length : 0
|
|
73
|
+
unless invocation[:arity].key?(params_number)
|
|
74
|
+
raise Fhirpath::Error,
|
|
75
|
+
"#{fn_name} wrong arity: got #{params_number}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
this_value = ctx[:this] || ctx[:root]
|
|
79
|
+
arg_types = invocation[:arity][params_number]
|
|
80
|
+
params = [ctx, data] + positional_params(ctx, this_value, arg_types, raw_params, params_number)
|
|
81
|
+
|
|
82
|
+
call_invocation(invocation, params)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def call_invocation(invocation, params)
|
|
86
|
+
return [] if invocation[:nullable] && params.any? { |param| Util.nullable?(param) }
|
|
87
|
+
|
|
88
|
+
Util.arraify(invocation[:fn].call(*params))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def positional_params(ctx, base_data, arg_types, raw_params, count)
|
|
92
|
+
(0...count).map { |i| make_param(ctx, base_data, arg_types[i], raw_params[i]) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def invocation_registry(ctx)
|
|
96
|
+
Invocations::REGISTRY.merge(ctx[:user_invocation_table] || {})
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|