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.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +25 -0
  4. data/CLAUDE.md +59 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +43 -0
  7. data/Rakefile +12 -0
  8. data/ext/fhir_path_parser/extconf.rb +40 -0
  9. data/lib/fhirpath/engine/evaluators/expressions.rb +59 -0
  10. data/lib/fhirpath/engine/evaluators/literals.rb +70 -0
  11. data/lib/fhirpath/engine/evaluators/member_invocation.rb +109 -0
  12. data/lib/fhirpath/engine/evaluators.rb +130 -0
  13. data/lib/fhirpath/engine/invocations/aggregate.rb +48 -0
  14. data/lib/fhirpath/engine/invocations/collections.rb +32 -0
  15. data/lib/fhirpath/engine/invocations/combining.rb +30 -0
  16. data/lib/fhirpath/engine/invocations/datetime.rb +34 -0
  17. data/lib/fhirpath/engine/invocations/equality.rb +95 -0
  18. data/lib/fhirpath/engine/invocations/equivalence.rb +138 -0
  19. data/lib/fhirpath/engine/invocations/existence.rb +117 -0
  20. data/lib/fhirpath/engine/invocations/filtering.rb +107 -0
  21. data/lib/fhirpath/engine/invocations/inequality.rb +92 -0
  22. data/lib/fhirpath/engine/invocations/logic.rb +74 -0
  23. data/lib/fhirpath/engine/invocations/math.rb +109 -0
  24. data/lib/fhirpath/engine/invocations/math_functions.rb +121 -0
  25. data/lib/fhirpath/engine/invocations/misc.rb +144 -0
  26. data/lib/fhirpath/engine/invocations/misc_converts_to.rb +91 -0
  27. data/lib/fhirpath/engine/invocations/navigation.rb +102 -0
  28. data/lib/fhirpath/engine/invocations/registry_strings_and_math.rb +48 -0
  29. data/lib/fhirpath/engine/invocations/strings.rb +139 -0
  30. data/lib/fhirpath/engine/invocations/strings_encoding.rb +68 -0
  31. data/lib/fhirpath/engine/invocations/subsetting.rb +15 -0
  32. data/lib/fhirpath/engine/invocations/types.rb +36 -0
  33. data/lib/fhirpath/engine/invocations.rb +118 -0
  34. data/lib/fhirpath/engine/nodes/fp_date_time.rb +140 -0
  35. data/lib/fhirpath/engine/nodes/fp_date_time_arithmetic.rb +112 -0
  36. data/lib/fhirpath/engine/nodes/fp_quantity.rb +142 -0
  37. data/lib/fhirpath/engine/nodes/fp_quantity_deep_equal.rb +67 -0
  38. data/lib/fhirpath/engine/nodes/fp_time.rb +139 -0
  39. data/lib/fhirpath/engine/nodes/fp_time_arithmetic.rb +111 -0
  40. data/lib/fhirpath/engine/nodes/resource_node.rb +66 -0
  41. data/lib/fhirpath/engine/nodes/type_info.rb +92 -0
  42. data/lib/fhirpath/engine/param_resolution.rb +74 -0
  43. data/lib/fhirpath/engine/util.rb +117 -0
  44. data/lib/fhirpath/engine.rb +100 -0
  45. data/lib/fhirpath/models/dstu2/choiceTypePaths.json +1041 -0
  46. data/lib/fhirpath/models/dstu2/path2Type.json +4728 -0
  47. data/lib/fhirpath/models/dstu2/pathsDefinedElsewhere.json +21 -0
  48. data/lib/fhirpath/models/dstu2/type2Parent.json +141 -0
  49. data/lib/fhirpath/models/r4/choiceTypePaths.json +1365 -0
  50. data/lib/fhirpath/models/r4/path2Type.json +7707 -0
  51. data/lib/fhirpath/models/r4/pathsDefinedElsewhere.json +57 -0
  52. data/lib/fhirpath/models/r4/type2Parent.json +211 -0
  53. data/lib/fhirpath/models/r5/choiceTypePaths.json +1883 -0
  54. data/lib/fhirpath/models/r5/path2Type.json +9653 -0
  55. data/lib/fhirpath/models/r5/pathsDefinedElsewhere.json +80 -0
  56. data/lib/fhirpath/models/r5/type2Parent.json +234 -0
  57. data/lib/fhirpath/models/stu3/choiceTypePaths.json +991 -0
  58. data/lib/fhirpath/models/stu3/path2Type.json +5729 -0
  59. data/lib/fhirpath/models/stu3/pathsDefinedElsewhere.json +37 -0
  60. data/lib/fhirpath/models/stu3/type2Parent.json +173 -0
  61. data/lib/fhirpath/models.rb +25 -0
  62. data/lib/fhirpath/parser/FHIRPath.g4 +172 -0
  63. data/lib/fhirpath/parser/ast_builder.rb +51 -0
  64. data/lib/fhirpath/parser.rb +21 -0
  65. data/lib/fhirpath/version.rb +5 -0
  66. data/lib/fhirpath.rb +117 -0
  67. data/rakelib/parser.rake +146 -0
  68. data/sig/fhirpath.rbs +14 -0
  69. 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