Coverage for src/lumen/fhirpath/engine/__init__.py: 76%
160 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-08-31 18:46 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-08-31 18:46 +0000
1import json
2import numbers
3import lumen.fhirpath.engine.util as util
4from lumen.fhirpath.engine.nodes import TypeInfo
5from lumen.fhirpath.engine.evaluators import evaluators
6from lumen.fhirpath.engine.invocations import (
7 invocation_registry as base_invocation_registry,
8)
11def check_integer_param(val):
12 data = util.get_data(val)
13 if int(data) != data:
14 raise Exception("Expected integer, got: " + json.dumps(data))
15 return data
18def check_number_param(val):
19 data = util.get_data(val)
20 if not isinstance(data, numbers.Number):
21 raise Exception("Expected number, got: " + json.dumps(data))
22 return data
25def check_boolean_param(val):
26 data = util.get_data(val)
27 if data is True or data is False:
28 return data
29 raise Exception("Expected boolean, got: " + json.dumps(data))
32def check_string_param(val):
33 data = util.get_data(val)
34 if not isinstance(data, str):
35 raise Exception("Expected string, got: " + json.dumps(data))
36 return data
39def do_eval(ctx, parentData, node):
40 node_type = node["type"]
42 if node_type in evaluators:
43 evaluator = evaluators.get(node_type)
44 return evaluator(ctx, parentData, node)
46 raise Exception("No " + node_type + " evaluator ")
49def doInvoke(ctx, fn_name, data, raw_params):
50 invocation_registry = {
51 **base_invocation_registry,
52 **(ctx["userInvocationTable"] or {}),
53 }
55 if isinstance(fn_name, list) and len(fn_name) == 1:
56 fn_name = fn_name[0]
58 if not isinstance(fn_name, str) or fn_name not in invocation_registry:
59 raise Exception("Not implemented: " + str(fn_name))
61 invocation = invocation_registry[fn_name]
63 if "nullable_input" in invocation and util.is_nullable(data):
64 return []
66 if "arity" not in invocation:
67 if raw_params is None or util.is_empty(raw_params):
68 res = invocation["fn"](ctx, util.arraify(data))
69 return util.arraify(res)
71 raise Exception(fn_name + " expects no params")
73 if invocation["fn"].__name__ == "trace_fn" and raw_params is not None:
74 raw_params = raw_params[:1]
76 paramsNumber = 0
77 if isinstance(raw_params, list):
78 paramsNumber = len(raw_params)
80 if paramsNumber not in invocation["arity"]:
81 raise Exception(fn_name + " wrong arity: got " + str(paramsNumber))
83 params = []
84 argTypes = invocation["arity"][paramsNumber]
86 thisValue = ctx["$this"] if "$this" in ctx else ctx["dataRoot"]
87 for i in range(0, paramsNumber):
88 tp = argTypes[i]
89 pr = raw_params[i]
90 params.append(make_param(ctx, thisValue, tp, pr))
92 params.insert(0, data)
93 params.insert(0, ctx)
95 if "nullable" in invocation:
96 if any(util.is_nullable(x) for x in params):
97 return []
99 # Allow per-arity function dispatch (e.g. sort() vs sort(expr))
100 fn = invocation.get("expr_dispatch", {}).get(paramsNumber, invocation["fn"])
101 res = fn(*params)
103 return util.arraify(res)
106_PRIMITIVE_TYPES = frozenset(
107 {
108 "base64Binary",
109 "boolean",
110 "canonical",
111 "code",
112 "date",
113 "dateTime",
114 "decimal",
115 "id",
116 "instant",
117 "integer",
118 "markdown",
119 "oid",
120 "positiveInt",
121 "string",
122 "time",
123 "unsignedInt",
124 "uri",
125 "url",
126 "uuid",
127 }
128)
131def is_valid_type(type_info, model):
132 name = type_info.name
133 ns = type_info.namespace
135 system_types = {
136 "String",
137 "Boolean",
138 "Integer",
139 "Decimal",
140 "Date",
141 "DateTime",
142 "Time",
143 "Quantity",
144 "Concept",
145 }
146 if ns == "System":
147 return True
149 if ns == "FHIR" or ns is None:
150 if name in system_types:
151 return True
152 if name in _PRIMITIVE_TYPES or name in (
153 "Quantity",
154 "Age",
155 "Count",
156 "Distance",
157 "Duration",
158 "Money",
159 "Range",
160 "Period",
161 "CodeableConcept",
162 "Reference",
163 ):
164 return True
165 if model:
166 if name in model.get("type2Parent", {}) or name in model.get("path2Type", {}).values():
167 return True
168 # Lazy import to avoid coupling lumen.fhirpath → lumen.fhir at import time
169 try:
170 from lumen.fhir.resource_types import known_resource_types
171 known = known_resource_types()
172 if name in known or name.capitalize() in known:
173 return True
174 except Exception:
175 pass
176 else:
177 return True
178 return False
181def type_specifier(ctx, parent_data, node):
182 identifiers = node["text"].replace("`", "").split(".")
183 namespace = None
184 name = None
186 if len(identifiers) == 2:
187 namespace, name = identifiers
188 elif len(identifiers) == 1:
189 (name,) = identifiers
190 else:
191 raise Exception(f"Expected TypeSpecifier node, got {node}")
193 type_info = TypeInfo(name=name, namespace=namespace)
194 model = ctx.get("model")
195 if not is_valid_type(type_info, model):
196 raise ValueError(f"Unknown type specifier: {type_info.name}")
197 return type_info
200param_check_table = {
201 "Integer": check_integer_param,
202 "Number": check_number_param,
203 "Boolean": check_boolean_param,
204 "String": check_string_param,
205}
208def make_param(ctx, parentData, node_type, param):
209 if node_type == "Expr":
211 def func(data):
212 ctx["$this"] = util.arraify(data)
213 return do_eval(ctx, ctx["$this"], param)
215 return func
217 if node_type == "AnyAtRoot":
218 ctx["$this"] = ctx["$this"] if "$this" in ctx else ctx["dataRoot"]
219 return do_eval(ctx, ctx["$this"], param)
221 if node_type == "Identifier":
222 if param["type"] == "TermExpression":
223 return param["text"]
225 raise Exception("Expected identifier node, got " + json.dumps(param))
227 if node_type == "TypeSpecifier":
228 return type_specifier(ctx, parentData, param)
230 ctx["$this"] = parentData
231 res = do_eval(ctx, parentData, param)
233 if node_type == "Any":
234 return res
236 if isinstance(node_type, list):
237 if len(res) == 0:
238 return []
239 else:
240 node_type = node_type[0]
242 if len(res) > 1:
243 raise Exception(
244 "Unexpected collection"
245 + json.dumps(res)
246 + "; expected singleton of type "
247 + node_type
248 )
250 if len(res) == 0:
251 return []
253 if node_type not in param_check_table:
254 raise Exception("Implement me for " + node_type)
256 check = param_check_table[node_type]
258 return check(res[0])
261def infix_invoke(ctx, fn_name, data, raw_params):
262 invocation_registry = {
263 **base_invocation_registry,
264 **(ctx["userInvocationTable"] or {}),
265 }
266 if fn_name not in invocation_registry or "fn" not in invocation_registry[fn_name]:
267 raise Exception("Not implemented " + fn_name)
269 invocation = invocation_registry[fn_name]
270 paramsNumber = len(raw_params)
272 if paramsNumber != 2:
273 raise Exception("Infix invoke should have arity 2")
275 argTypes = invocation["arity"][paramsNumber]
277 if argTypes is not None:
278 params = [ctx]
280 for i in range(0, paramsNumber):
281 argType = argTypes[i]
282 rawParam = raw_params[i]
283 params.append(make_param(ctx, data, argType, rawParam))
285 if "nullable" in invocation:
286 if any(util.is_nullable(x) for x in params):
287 return []
289 res = invocation["fn"](*params)
290 return util.arraify(res)
292 print(fn_name + " wrong arity: got " + paramsNumber)
293 return []