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,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